Scalaのパターンマッチでの@の挙動

List(1, 2) match { case Seq(xs @ _*) => println(xs) } の意味が果てしなくわからないんだけどなにこれ

という声を聴いた。たしかにScalaのパターンマッチにおいて @ の挙動はわかりにくいかもしれない。

Scalaのパターンマッチでは、@を利用して2回変数束縛を行うことができる。

たとえば@を使わないパターンマッチの例としては

Option(1) match {
  case Some(x) => x * 2
  case None => 0 
}

// => Int =2

といったものが考えられる。今はSomeの中身をxに束縛したが、@を使うことで、中身をxに束縛しつつSome自体も別の変数に束縛する、というようなことができる

Option(1) match {
  case s @ Some(x) =>
    println(s)
    println(x)
  case None => 
    ()
}

// Some(1)
// 1

簡単にいうと、「ふつうなら一回しか変数束縛できないけど、@を使うとチャンスが2倍!!」みたいな感じ(後述するが、これは正確な説明ではない)。

「変数束縛が2回できてお得でっせ!」というわけで、意味はないが、同じ値を複数の変数に束縛することもできる

1 match {
  case x @ y => x + y
}

// => Int = 2

また、case節のトップレベルだけではなく、中にも(つまりパターンが期待されているところに)書ける

Option(1) match {
  case Some(x @ y) => x + y
  case None => 0
}

// => Int = 2

ところで、@によって2回変数束縛できてお得!!!とは言ったが、実はこれは正確ではない。なぜなら、@の前にはパターンではなく変数しか置けないからである。

Pattern Matching | Scala 2.12の8.1.3、Pattern Bindersを見てほしい。

A pattern binder x@p consists of a pattern variable x and a pattern p. The type of the variable x is the static type T of the pattern p. This pattern matches any value v matched by the pattern p, provided the run-time type of v is also an instance of T, and it binds the variable name to that value.

とある通り、 x @ pxp というパターンによって表される型の変数である。パターンではない。

なので、「2回束縛できてお得!」というのはあまり正確ではなくて、@の前に「@の後ろに書かれたパターン全体にマッチしたものを束縛する変数」がかけて、そのあとに細かいパターンを書ける。結果として、パターン全体にマッチしたものが束縛される変数と、パターンによって細かく束縛された変数がどちらも得られるという感じである。実際、以下のようなパターンはコンパイルエラーとなる。Some(x)はパターンであって変数ではないからである。

Option(1) match {
  case Some(x) @ Some(y) => x + y
  case None => 0
}

というわけで、冒頭の「List(1, 2) match { case Seq(xs @ _*) => println(xs) }」については、まず List(1, 2)の中身に、@の後ろのパターン_* がマッチし、マッチした全体をxsに束縛する、という挙動をすることになる。List(1, 2) match { case Seq(_*) => println("hoge") } では、Listの中身が取り出せないが、List(1, 2) match { case Seq(xs @ _*) => println(xs) }ならば中身をxsという変数に束縛できる、というわけだ。