Scala初心者がポルナレフ状態に陥らないために知っておくべきこと

この記事はPlay or Scala Advent Calendar 2012の13日目の記事です。Advent Calendar も折り返し地点になります。

ScalaはJavaに比べて非常に簡潔なコードを書けることが魅力のひとつですが、簡潔に書こうとして想定しない動作になってしまったことのひとつやふたつ誰にでもあるはず。

Exsample

さて、以下のREPLの結果を見てください。

[code language="scala"] scala> val list1 = List(1, 2, 3)
list1: List[Int] = List(1, 2, 3)

scala> val list2 = List(4, 5, 6)
list2: List[Int] = List(4, 5, 6)

scala> val list3 = list1:::list2.filter(_ % 2 1)
list3: List[Int] = List(1, 2, 3, 5)
[/code]

俺はList(1, 2, 3) と List(4, 5, 6) を結合して奇数のみ抽出しようとしたのに、なぜか2がフィルターできていなかった…。なんてことになることがあります。

何が起こっているのか?

Scalaist の皆さんならご存知の通り、Scalaだと '1 + 2' の '+' のように、演算子に見えるものも全て関数です。また、全ての関数が演算子と同じような書き方で呼び出せます。平たく言うと、ピリオドを省略できます。上の例では:::関数はピリオドを省略していますが、filter関数の呼び出しはピリオドをつけています。

何が起こっているのかよりわかりやすくするため、Stringで同じようなことをやってみましょう。

[code language="scala"] scala> val str1 = "abc"
str1: java.lang.String = abc

scala> val str2 = "def"
str2: java.lang.String = def

scala> val str3 = str1 + str2.substring(1, 2)
str3: java.lang.String = abce
[/code]

どうでしょうか。こちらの例だと直感的にわかると思いますが、str3 は str1 に str2.substring(1, 2) を足したものになっています。これと同じことが上のListの例でも起こっているわけです。 つまり、list2 から奇数だけを抽出したあとにlist1と結合しているわけですね。

どうすればいいか

なんでこんなことになってしまったのか理解できてしまえば簡単です。 [code language="scala"] scala> val list4 = (list1:::list2).filter(_ % 2 1)
list4: List[Int] = List(1, 3, 5)
[/code] 括弧をつければ、括弧内の処理結果に対して括弧外の処理を行わせることができます。他にも、list1 と list2 を結合したListを変数に格納してからfilterしてもいいですね。 ただ、最初に書いたコードと比べると括弧をつけた分だけ冗長な感じもします。それがどーしても許せない人はこんな書き方もできます。 [code language="scala"] scala> val list5 = list1:::list2 filter(_ % 2 == 1)
list5: List[Int] = List(1, 3, 5)
[/code] filter関数の呼び出しのピリオドを省略してしまう。この書き方でもlist1とlist2を結合してからfilterが走ります。まぁ後からコードを読んだときのことを考えると、括弧をつけた方が無難な気がしますが…。

まとめ

Scalaでは1行の中にピリオド有と無が混在している場合、ピリオド有の関数が優先して呼ばれます。演算子系の関数はわかりやすいですが、Listの:::などだと一瞬ポルナレフ状態に陥ることもあるので気を付けましょう。