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