Scalaの `:+` と `+:` にまつわる話

これはClassi Advent Calendar 2018の3日目の記事です。

Classiでいろいろやってるしんぺいです。最近は「Scala入学式」と称してScalaに入門してもらう社内勉強会を主催しています。

というわけで今日もScalaの話です。

Seqの +::+

Scalaには順序を持ったコレクションの抽象としてSeqというtraitが定義されています。そして、そのSeqの先頭に要素を追加するのに +: が、最後に要素を追加するのには :+ が使えます。

val s = Seq(1, 2, 3)
println(0 +: s) // => List(0, 1, 2, 3)
println(s :+ 4) // => List(1, 2, 3, 4)

Seq は抽象なので、デバッグプリントすると具象クラスであるところの List が表示されますが、List is a Seq なので期待通りです。

一見「演算子」に見える +::+ ですが、じつはこれは Seq のメソッドです。まずはわかりやすい :+ から見てみましょう。

val s = Seq(1, 2, 3)
println(s :+ 4) // => List(1, 2, 3, 4)
println(s.:+(4)) // => List(1, 2, 3, 4)

s :+ 4 と書いた場合も s.:+(4) と書いた場合も同じ結果を返しています。というわけで、じつはscalaでは、object.method(arg)object method argとも書けるのですね。これは :+ メソッドに独特の挙動ではなく、ほかのメソッドでも同じことです。

println("myself".splitAt(2)) // => (my, self)
println("myself" splitAt 2) // => (my, self)

では +: についてはどうでしょうか。

val s = Seq(1, 2, 3)
println(s.+:(0)) // => List(0, 1, 2, 3)
println(0 +: s) // => List(0, 1, 2, 3)

なんと、object method arg という書き方ではなく、 arg method object という書き方になっています!

これはなぜかと言うと、scalaにおいて : で終わるメソッドの場合、dotなしの記法だと arg method object という書きかたになるというルールがあるからです。このようなルールがあるとなぜうれしいのでしょうか。じつは +::+ を同時に使うと嬉しさが伝わってきます。

val s = Seq(1, 2, 3)
println(0 +: s :+ 4) // => List(0, 1, 2, 3, 4)

「先頭に0を、末尾に4を追加したSeqを返している」というのがとてもわかりやすく表現されているのではないでしょうか。こういうようなときに、たしかに arg method objectという書き方ができると嬉しいかもしれません。

+::+とパターンマッチ

さて、+::+ はパターンマッチでも出てきます。

val s = Seq(0, 1, 2, 3)
s match {
 case _ +: xs :+ _ => println(xs)
 case _ => ()
} // => List(1, 2)

先頭要素 +: Seq :+ 末尾要素 で、「Seqの先頭に先頭要素を、末尾に末尾要素を追加Seq」が構築できるのと同じような感じで、パターンマッチで 先頭要素 +: Seq :+ 末尾要素 を使うことでSeqを分解することができています。さきほどの +: と  :+ の正体はメソッドでしたが、パターンマッチ内ではもちろんメソッドでマッチさせることはできません。ではこれらの正体は一体なんなのでしょう。

じつはこれらはobjectとして定義されています。定義を見に行きましょう。

https://www.scala-lang.org/api/current/scala/collection/$plus$colon$.html

https://www.scala-lang.org/api/current/scala/collection/$colon$plus$.html

unapllyが定義された+:というobjectと:+が定義されていることがわかると思います。

パターンマッチは内部でunapplyを呼び、成功した場合「マッチした」とみなして返り値をそれぞれの変数にbindするような挙動をしますが、+:というobjectや:+というobjectがunapplyを持っているおかげで、パターンマッチ内でこの「演算子に見えるようななにか」が使えるというからくりなのでした。

まとめ

Scala+::+ について見てきました。これらの「演算子に見えるようななにか」は、じつは普段はSeqのメソッドであり、パターンマッチの中ではunapplyを持ったobjectとして振る舞っていたんですね!「普段構築するとき」と「パターンマッチで分解するとき」の対応が取れているため、利用する側としては違和感なく使えていますが、じつは内部ではこんな工夫がなされているんだよ、というのが伝われば幸いです。