matchで型判定がおかしいのはコンパイルのせいで、型タグで解決する

match が意図した挙動にならない

突然ですが下記プログラムの実行結果は何になるでしょうか?

  case class Robot()
  val robo = new Robot
  
  robo match {
    case _: Robot => println("Robot!")
    case l: List[String] => println(s"List String! $l")
    case l: List[Int] => println("List Number!")
    case _ => println("other")
  }

答えはRobot!です。

では次の実行結果は何になるでしょうか?

  List(1,2,3) match {
    case _: Robot => println("Robot!")
    case l: List[String] => println(s"List String! $l")
    case l: List[Int] => println(s"List Number! $l")
    case _ => println("other")
  }

答えはList Number! List(1, 2, 3)ではなく、List String! List(1, 2, 3)!になるのです。

なぜ

下記理由です。

他の JVM言語同様に、Scala の型はコンパイル時に消去 (erase) される。 これは、何らかのインスタンスのランタイム型をインスペクトしてもコンパイル時に Scala コンパイラが持つ型情報を全ては入手できない可能性があることを意味する。 型タグとマニフェスト | Reflection | Scala Documentation

List[T]のTが消えてしまって、一番最初のList[String](型情報が失われているので、List[T]にマッチしている?)にマッチしていると思われます。

対応方法

先述の 型タグとマニフェスト | Reflection | Scala Documentationに書いてあるのですが、型情報を保持してList[Int]にマッチさせるにはTypeTagを使えば良い。

  import scala.reflect.runtime.universe._

  def printListType[T: TypeTag](value:List[T]) :Unit = typeOf[T] match {
    // =:= は型が等しいかを判定
    case l if l =:= typeOf[String] =>  println(s"List String!")
    case l if l =:= typeOf[Int] => println("List Number!")
    case _ => println("other")
  }

  // 型によってprintされる文字が変わった
  printListType(List(1,2,3)) // List Number!
  printListType(List("str")) // List String!

参考