Scalaにおける括弧()と中括弧{}の違い (squerylでハマった備忘録)

主にSqueryl( version 0.9.5-6 )の例で話しますが、括弧の違いの話はSquerylに限らずScala一般的な話です。


例えば、squerylのドキュメントのイントロダクションのページにこんな例があります。whereで条件絞り込んで、その後scoreInPercentageというfieldの平均求めています。

val avg: Option[Float] =
  from(grades)(g =>
    where(g.subjectId === mathId)  
    compute(avg(g.scoreInPercentage))
  )

これを{}を使って以下のように書くと

val avg: Option[Float] =
  from(grades){g =>
    where(g.subjectId === mathId)  
    compute(avg(g.scoreInPercentage))
  }

全く別の意味になってバグる
という話です。以下基本的にこれの解説するので、上記の2つの違いが完全に分かる人は、この後読む必要ありません。


Scalaではセミコロン省略できます。なので、さっきの後者の例は以下と同等です

  from(grades){g =>
    where(g.subjectId === mathId);
    compute(avg(g.scoreInPercentage))
  }

つまりせっかく書いたwhere句は無視*1されて、全部の平均を求めてしまいます。
しかし、最初の例は

val avg: Option[Float] =
  from(grades)(g =>
    where(g.subjectId === mathId).  // ←ここに . が入ってることに注目
    compute(avg(g.scoreInPercentage))
  )

と同じです。whereで返ってきたオブジェクトに対してのcomputeというメソッド呼び出しになります。

とてもわかりにくいというか、混乱しやすいというか、実際混乱しました!


Scalaの言語仕様的に文法の正しい説明する自信がないというか仕様調べるの面倒なので(誰か補足して!)雑になんとなく(?)説明すると、例えば以下の2つを実行すると片方はコンパイルできて、片方はエラーになります。

Welcome to Scala version 2.10.0 (Java HotSpot(TM) 64-Bit Server VM, Java 1.7.0_09).
Type in expressions to have them evaluated.                                        
Type :help for more information.                                                   
                                                                           
scala> List(1).map{a => val b = a * 2 ; b}                                         
res0: List[Int] = List(2)                                                          
                                                                                   
scala> List(1).map(a => val b = a * 2 ; b)                                         
<console>:1: error: illegal start of simple expression                             
       List(1).map(a => val b = a * 2 ; b)                                         
                        ^                                                          
<console>:1: error: ')' expected but ';' found.                                    
       List(1).map(a => val b = a * 2 ; b)                                         
                                      ^                                            
<console>:1: error: ';' expected but ')' found.                                    
       List(1).map(a => val b = a * 2 ; b)                                         
                                         ^                                         

無名関数書くときに、{}と()で書くのはこのように微妙な違いがあります。
()で書く場合は、一つの式でないとエラーになります。

squerylの最初の()で書いた例は、改行を入れても

where(g.subjectId === mathId);
compute(avg(g.scoreInPercentage))

ではなく

where(g.subjectId === mathId).compute(avg(g.scoreInPercentage))

と、1つの式として認識されます。

squerylにおいて、

  • whereで返ってきたオブジェクトに対してcomputeやselectが呼べる、
  • なおかつ、PrimitiveTypeMode._ というのをimportすれば、直接compureやselectが呼べる

ので、where句が捨てられていてもエラーにならずに呼べてしまってとてもわかりにくいです。

自分は、Scalaで無名関数を書く場合、なんとなく()ではなく{}を使うことが多いのですが、
SquerylのこのDSLを使う場合は必ず()で書いたほうがよさそうです
(もしくは、必ずオブジェクトとメソッドの間の . を省略しないようにする)

たしかに、squerylのサンプルコード(やドキュメント)を今一度見てみると、全部()で書いてありました・・・

https://github.com/max-l/Squeryl/blob/0.9.5-6/src/test/scala/org/squeryl/test/demo/MusicDb.scala#L66-L126



以下反応など追記

だれかこの件に関して、squerylのメーリングリストでの議論か、ドキュメントでこの話でてるところあったら教えて下さい

*1:whereの部分は副作用を起こすわけではなく、そこから返ってきたオブジェクトに対してメソッドチェーンして使うことを想定されているので、これでは意味が無い