Scalaのmatch caseが怖いと思ったら怖くなかった話

おさらい

Scalaのmatch case式がある。他の言語で言うところのswitch caseみたいな立ち位置だが、実際には比較にならないほど強力である。

def f(n: Int): String =
  n match {
    case 1 => "いち"
    case 2 => "に"
    case _ => "1と2以外" // default値
  }

普通の値比較はこう。match「式」なので、返り値も持てる。_はそれより上で引っ掛からなかった値が全部処理される。勿論比較対象はStringじゃ使えないとか良く分からない制限はない。

次からはScala特有だが、まず、変数に束縛できる。

def f(n: Int): String =
  n match {
    case 1 => "いち"
    case 2 => "に"
    case x => x.toString
  }

_を変数名に置き換えただけのもの。Tupleや配列などの一部分にmatchすることもできる。(抽出子によるパターンマッチ)

def f(t: (Int, String)): String =
  t match {
    case (1, _) => "いち"
    case (2, str) => str
    case (x, _) => x.toString
  }

この場合、intが1であればstringが何であれ"いち"を、2であればstringをそのまま、それ以外であればintをtoStringしたものを返す。これらはunapply(というより抽出子か)で実現されているが、詳細はもっと良い他の資料を見ると良い(ググレカスの婉曲表現)

他にも型で分岐することもできる。

def f(x: AnyVal): String =
  x match {
    case y: String => y
    case y: Int => y.toString
    case _ => "Unknown Type"
  }

ifっぽく分岐したいならガード条件のifを使えばいい。

def f(x: AnyVal): String =
  x match {
    case y: Int if y > 0 => "Over 0"
    case y: Int => "Under 0"
    case _ => "Unknown Type"
  }

Haskellでas Patternと呼ばれている@も使える。

def f(t: (Int, String)): String =
  t match {
    case x @ (i, "OK") =>
      assert(x._1 == i)
      x.toString
    case x @ (1, str) =>
      assert(x._2 == str)
      str
    case _ => "Other"
  }

@前のxに全体が置かれつつも、中身は中身でちゃんとチェックしたりヒットしたりしてくれる。ただしおこの場合、全体をヒットさせるならtで良いので、多重Tupleとかでないと活躍しないかもしれない。

本題のこわいところ

以上のようにmatch caseには様々な使い方があり大変高機能なのだが、多機能故に、複数の意味に取れるパターンがある。

特に「特定の値にヒットする」caseと「変数に束縛する」caseがどっちとも取れる場合が割と良くある。

シンプルな例だと

val x = 2
1 match {
  case x => x
  case _ => -1
}

の結果は、幾つかのWarningと共に1が出力される筈だ。このxは束縛の意味になるから全ての場合でヒットする。下の行が無意味だという警告が出ている筈だ。

ではこれは

val X = 2
1 match {
  case X => X
  case _ => -1
}

-1が返ってきて、先程と違う結果になる。何故ならば、頭が大文字である場合は定数として扱われ、「特定の値にヒットする」方が優先されるからである。もちろん2でmatchさせれば2が返る。

ひねくれてこんなの考えてみる。

object math {
  val Two = 2
}

2 match {
  case math.Two => "に"
  case _ => "それ以外"
}

ちゃんと「に」が返ってくる。

じゃあこんなのできるんじゃね!?ってなる

case class Pos(x: Int, y: Int)
val p = Pos(1, 2)

1 match {
  case p.x => "x"
  case p.y => "y"
  case _ => "other"
}

普通に通る(xが返ってくる)

で、これできないパターンあるんやで!っていうのを挙げようとしたけど何故か見つけられなかったので、怖い話をするつもりだったけど普通にこわくない話になった。というオチ