Set + Tuple = ?

この記事はScala Advent Calendar (http://atnd.org/events/10683)の27日のものです。


簡単な記事です。
さて、以下の式はどの様な結果を生成するでしょうか?

Set() + (1,2)


プログラマ(私)の意図としては、Set( (1,2) ) ((1,2)の1要素タプルを持つ集合)を作って欲しかった一文です。
しかしながら、この文は予想に反してSet(1,2) (1, 2を持つSet[Int]型の集合)を生成します。


なぜでしょうか?
その理由を知るためにSet型の+メソッドを見てみましょう。
すると、以下の様なオーバーロードが存在することが分かります。

def + (elem1: A, elem2: A, elems: A*) : Set[A]
def + (elem: A) : Set[A]

http://www.scala-lang.org/api/current/scala/collection/Set.html

もうお分かりだとは思いますが、scala演算子はメソッド呼び出しの置き換えです。
その際、引数の()が1つ*1である場合は、ドットを省略出来るようです。(不確かなので、正確な情報を知っている方教えて下さい)

// 検証コード
(1 to 10) sortWith (_>_)     // OK
(1 to 10) foldLeft (0) (_+_) // NG
(1 to 10).foldLeft (0) (_+_) // OK


要するに、Set() + (1,2)は Set().+(1,2)に置き換えられており、その結果としてSet(1,2)が生成されていた訳です。
コレと同様のミスは型を指定しているコードではコンパイルエラーとなって現われます。
Setの要素として(Int, Int)を要求しているにも関わらず、メソッド呼び出しで2つのIntを与えているためにコンパイルエラーに成るわけです。

scala> val set = Set[(Int, Int)]()
scala> set + (1,2)
<console>:8: error: type mismatch;
 found   : Int(1)
 required: (Int, Int)
       set + (1,2)


私が使っている限りでは、foldLeftなどのループ処理中にSet要素を追加したい事が結構あったので、そこでハマった現象です。
解決作としては、以下の方法が考えられます。

// 1) 一度タプルを変数に代入する
scala> val v = (1,2)
scala> Set() + v
res33: scala.collection.immutable.Set[(Int, Int)] = Set((1,2))

// 2) タプルである事を更に括弧を付けて示す
scala> Set() + ((1,2))
res34: scala.collection.immutable.Set[(Int, Int)] = Set((1,2))

// 3) 仮引数を使って括弧内の要素を分かり安くする
scala> Set() + (elmt = (1,2))
res35: scala.collection.immutable.Set[(Int, Int)] = Set((1,2))

// 4) タプルの別表現を用いる
// thanks id:xuweiさん
scala> Set() + Tuple2(1, 2)
scala> Set() + (1 -> 2)
res36: scala.collection.immutable.Set[(Int, Int)] = Set((1,2))

私的には1)が良いのではないかと思います。
単純にタプルを渡すだけのメソッドでも、同様のミスが現われる可能性は大きいと言えます。
そのため、タプルをメソッドに渡す場合、一時的な変数を用意して利用する(1)のパターンを利用する)と安全でしょう。

追記:また、タプルか引数かの曖昧さを排除した4)も良いと思います。

*1:Scalaではカリー化によって2つ以上の括弧による関数定義が可能