Why can't I pattern match on generics in Scala using TypeTags

Viewed 73

I am probably overlooking something but I'm wondering the following. Using TypeTags in Scala 2.13 I can do this:

def fun[T: TypeTag](v: List[T]) = {
  v match {
    case v2: List if typeOf[T] =:= typeOf[Option[Int]] => {
      val v3 = v2.asInstanceOf[List[Option[Int]]]
      v3.map(x: Int => ...)
    }
    ...
  }
}

In this example I can use v3 with the correct type and I can be sure that the cast succeeds since I checked it before via the TypeTag. But it feels broken since I have to enter the type twice (once in the check and once in the cast) which can lead to typo errors and it seems like unnecessary boilerplate.

So is there any reason why the compiler doesn't support me writting

def fun[T: TypeTag](v: List[T]) = {
  v match {
    case v3: List[Option[Int] => {
      v3.map(x: Int => ...)
    }
    ...
  }
}

and then fills in the check and the cast for me?

It seems straight forward to me, but, as I said, I'm probably overlooking something.

Thanks in advance for any helpful explanation.

1 Answers

But it feels broken since I have to enter the type twice

This is just the way it works, since it's implemented with an if guard. While you can use type matching to narrow the type of the right-hand side of a match (like case x: Int), this only works for non-erased types. The if guard can correctly narrow a successful match to the result you expect by comparing type tags, but it doesn't narrow the type of the right-hand side, and for good reason. Since the if guard is just a Boolean expression, the compiler would need to do quite a bit of gymnastics to determine your intention to filter by type. Imagine you wrote:

if typeOf[T] =:= typeOf[Option[Int]] || typeOf[T] =:= typeOf[Int]

Things can get complicated fairly quickly for the compiler by trying to correctly infer the type this way. Because of this, a cast is necessary.

You could sweep some of the casting under the rug by creating an extractor to handle it for you:

object TypeMatch {
  def apply[A: TypeTag]: TypeMatch[A] = new TypeMatch[A]
}

class TypeMatch[A: TypeTag] {
  def unapply[B: TypeTag](b: List[B]): Option[List[A]] = {
    if(typeOf[A] =:= typeOf[B]) Option(b.asInstanceOf[List[A]])
    else None
  }
}

def fun[T: TypeTag](v: List[T]) = {
  val OptionInt = TypeMatch[Option[Int]]
  v match {
    case OptionInt(v3) => {
      v3.foreach((x: Option[Int]) => println(x))
    }
    case _ => println("Not List[Option[Int]]")
  }
}

This isn't particularly pretty, and I probably wouldn't bother using it unless I was casting over and over again. Unfortunately, the extractor needs to have a stable identifier, so we can't write case TypeMatch[Option[Int]](list) or anything like that, but it works:

scala> fun(ints)
Some(1)
Some(2)
None
None
Some(10)
Some(20)

scala> fun(strings)
Not List[Option[Int]]
Related