How to pattern match on generic type in Scala?
Asked Answered
D

4

31

Let's suppose we have a generic class Container:

case class Container[+A](value: A)

We then want to pattern match a Container with a Double and a Container of Any:

val double = Container(3.3)  
var container: Container[Any] = double

To do this, we would normally write:

container match {  
  case c: Container[String] => println(c.value.toUpperCase)
  case c: Container[Double] => println(math.sqrt(c.value))  
  case _ => println("_")  
}

However, the compiler gives two warnings, one for each of the first two cases. For example, the first warning says: "non-variable type argument String in type pattern Container[String] is unchecked since it is eliminated by erasure". Because of the erasure, it is impossible during runtime to distinguish between different kinds of containers and the first catch will be matched. As a consequence, container of type Container[Double] will be matched by the first case, which catches Container[String] objects, so toUpperCase method will be called on a Double and a java.lang.ClassCastException will be thrown.

How to match a Container parametrized by a particular type?

Denumerable answered 17/4, 2013 at 9:39 Comment(1)
I've added an answer to the same question over there: linkExtraordinary
M
33

Maybe this will help

 def matchContainer[A: Manifest](c: Container[A]) = c match {
      case c: Container[String] if manifest <:< manifest[String] => println(c.value.toUpperCase)
      case c: Container[Double] if manifest <:< manifest[Double] => println(math.sqrt(c.value))
      case c: Container[_] => println("other")
    }

Edit:

As Impredicative pointed out, Manifest is deprecated. Instead you could do the following:

import reflect.runtime.universe._
def matchContainer[A: TypeTag](c: Container[A]) = c match {
      case c: Container[String] if typeOf[A] <:< typeOf[String] => println("string: " + c.value.toUpperCase)
      case c: Container[Double] if typeOf[A] <:< typeOf[Double] => println("double" + math.sqrt(c.value))
      case c: Container[_] => println("other")
    }
Macneil answered 17/4, 2013 at 9:55 Comment(1)
Worth noting that Manifest is deprecated in favour of TypeTag in recent versions of Scala.Whiteside
A
32

In general rarry's answer is correct, for your case however it can be simplified, because your container only contains a single value of a generic type, so you can match on that value's type directly:

container match {
  case Container(x: String) => println("string")
  case Container(x: Double) => println("double")
  case _ => println("w00t")
}
Adjourn answered 17/4, 2013 at 10:4 Comment(0)
D
13

A possible workaround for this could be to use isInstanceOf and asInstanceOf.

container match {  
  case Container(x) if x.isInstanceOf[String] =>  
    println(x.asInstanceOf[String].toUpperCase)  
  case Container(x) if x.isInstanceOf[Double] =>  
    println(math.sqrt(x.asInstanceOf[Double]))  
  case _ => println("_")  
}

This works, but it doesn't look elegant at all. Professor Martin Odersky, the creator of Scala, says that isInstanceOf and asInstanceOf should be avoided.

As Rob Norris pointed me out, on the forum of the course "Functional programming in Scala" from Coursera, matching by type is a bad practice: case foo: Bar => .... Scala encourages to take advantage of static typing and avoid checking type during runtime. This is consistent with the philosophy of Haskell/ML world. Instead of matching types, case clauses should match constructors.

To solve the Container matching problem, a special container for each type can be defined:

class Container[+A](val value: A)

case class StringContainer(override val value: String)
  extends Container(value)

case class DoubleContainer(override val value: Double)
  extends Container(value)

And now constructors will be matched, not types:

container match {
  case StringContainer(x) => println(x.toUpperCase)
  case DoubleContainer(x) => println(math.sqrt(x))
  case _ => println("_")
}

Apparently, we could be defined unapply methods in two objects, StringContainer and DoubleContainer and use the same match as above, instead of extending the Container class:

case class Container[+A](val value: A)

object StringContainer {
  def unapply(c: Container[String]): Option[String] = Some(c.value)
}


object DoubleContainer {
  def unapply(c: Container[Double]): Option[Double] = Some(c.value)
}

But this does not work, again, because of JVM type erasure.

A reference to Rob Norris post, which lead me to this answer can be found here: https://class.coursera.org/progfun-002/forum/thread?thread_id=842#post-3567 . Unfortunately, you can't access it unless you are enrolled in the Coursera course.

Denumerable answered 17/4, 2013 at 9:39 Comment(0)
P
5

Note: you also have an alternative with Miles Sabin's Shapeless library (already mentioned by Miles in 2012 here).

You can see an example in "Ways to pattern match generic types in Scala" from Jaakko Pallari

Typeable is a type class that provides the ability to cast values from Any type to a specific type.
The result of the casting operation is an Option where the Some value will contain the successfully casted value, and the None value represents a cast failure.

TypeCase bridges Typeable and pattern matching. It's essentially an extractor for Typeable instances

import shapeless._

def extractCollection[T: Typeable](a: Any): Option[Iterable[T]] = {
  val list = TypeCase[List[T]]
  val set  = TypeCase[Set[T]]
  a match {
    case list(l) => Some(l)
    case set(s)  => Some(s)
    case _       => None
  }
}

val l1: Any = List(1, 2, 3)
val l2: Any = List[Int]()
val s:  Any = Set(1, 2, 3)

extractCollection[Int](l1)    // Some(List(1, 2, 3))
extractCollection[Int](s)     // Some(Set(1, 2, 3))
extractCollection[String](l1) // None
extractCollection[String](s)  // None
extractCollection[String](l2) // Some(List()) // Shouldn't this be None? We'll get back to this.

While Typeable may look like it has what it takes to solve type erasure, it's still subject to the same behaviour as any other runtime code.
This can be seen in the last lines of the previous code examples where empty lists were recognized as string lists even when they were specified to be integer lists. This is because Typeable casts are based on the values of the list. If the list is empty, then naturally that is a valid string list and a valid integer list (or any other list for that matter)

Plumbago answered 14/6, 2016 at 16:32 Comment(1)
The linked article really gives an excellent treatment of this topic, and also covers the other alternatives (runtime reflection, etc.)Gobbet

© 2022 - 2024 — McMap. All rights reserved.