I would like to pattern match a function, the problem is type erasure. Notice how in the snippet below, despite the warning issued a match occurs and a "wrong" one at that.
scala> def f1 = ()=>true
f1: () => Boolean
scala> val fl = f1
fl: () => Boolean = <function0>
scala>
scala> fl match {
| case fp :Function0[Boolean] => 1
| case _ => 2
| }
res8: Int = 1
scala>
scala> fl match {
| case fp :Function0[String] => 1
| case _ => 2
| }
<console>:11: warning: fruitless type test: a value of type () => Boolean cannot also be a () => String (but still might match its erasure)
case fp :Function0[String] => 1
^
res9: Int = 1
scala>
What I could come up with is a case class wrapping up the function. I get type safety, notice the error below. But, this is, first, inelegant and second, I don't understand how the case class can enforce types whereas the pattern match can't. The only guess I would have is that the case class is protected by the compiler and that the match is only resolved against during runtime
scala> case class FunctionWrapper(fn: ()=>Boolean)
defined class FunctionWrapper
scala> val fw = FunctionWrapper(fl)
fw: FunctionWrapper = FunctionWrapper(<function0>)
scala> def fs = ()=>"whatever"
fs: () => String
scala> val fws = FunctionWrapper(fs)
<console>:10: error: type mismatch;
found : () => String
required: () => Boolean
val fws = FunctionWrapper(fs)
^
scala> fw match {
| case FunctionWrapper(f) => f()
| case _ => false
| }
res10: Boolean = true
To sum up, I would like to know if there is an elegant way to pattern match a function, and perhaps understand why the examples above acted as they did
The short answer: You've got to undo the erasure be reifying the types with TypeTag
.
I don't understand how the case class can enforce types whereas the pattern match can't.
Because your case class has no type parameters. Only generic types are erased, which is why it's called "partial erasure".
Related question: Generic unapply method for different types of List. The following code is essentially the same as one of the answers there, but using functions instead of lists:
import scala.reflect.runtime.universe._
def foo[A : TypeTag](a: A): Int = typeOf[A] match {
case t if t =:= typeOf[Int => Int] => a.asInstanceOf[Int => Int](0)
case t if t =:= typeOf[Boolean => Int] => a.asInstanceOf[Boolean => Int](true)
case _ => 3
}
foo((i: Int) => i + 1)
// res0: Int = 1
foo((b: Boolean) => if (b) 2 else 0)
// res1: Int = 2
foo((b: Boolean) => !b)
// res2: Int = 3
I'm not sure whether there's a way to write an extractor to make the match block nicer.
If you need to pass these functions in a way that loses the static type information (shoving them into a collection of Function[_, _]
, using then as Akka messages, etc.) then you need to pass the tag around too:
import scala.reflect.runtime.universe._
case class Tagged[A](a: A)(implicit val tag: TypeTag[A])
def foo[A, B](tagged: Tagged[A => B]): Int = tagged.tag.tpe match {
case t if t =:= typeOf[Int => Int] => tagged.a.asInstanceOf[Int => Int](0)
case t if t =:= typeOf[Boolean => Int] => tagged.a.asInstanceOf[Boolean => Int](true)
case _ => 3
}
foo(Tagged((i: Int) => i + 1))
// res0: Int = 1
foo(Tagged((b: Boolean) => if (b) 2 else 0))
// res1: Int = 2
foo(Tagged((b: Boolean) => !b))
// res2: Int = 3
The warning here is actually two-fold:
1) First, "a value of type () => Boolean cannot also be a () => String": indeed you are matching against a () => Boolean
and it can never be at the same time a () => String
so the case does not make sense, and in an ideal world should never match. However erasure comes into play as the second part hints at
2) "(but still might match its erasure)": erasure here means that instances of () => Boolean
(aka Function0[Boolean]
) and instances of () => String
(aka Function0[String]
) are represented exactly the same at runtime. Thus there is no way to distinguish them and when you pattern match against Function0[String]
in reality the compiler can only tell that it is some Function0
but cannot know if it is Function0[Boolean]
or Function0[String]
.
Admitedly the second part of the warning was easy to miss here.
Had fl
be typed Any
, the first part of the warning would not apply, and you would have got a more usefull message:
scala> (fl:Any) match {
| case fp :Function0[Boolean] => 1
| case _ => 2
| }
<console>:11: warning: non-variable type argument Boolean in type pattern () => Boolean is unchecked since it is eliminated by erasure
case fp :Function0[Boolean] => 1
As for a solution, there is little you can do except indeed wrapping the function instance. Luckily, you don't need to write one specific wrapper for every possible return type. Scala provides ClassTag
and TypeTag
to work around erasure, and we can take advantage of it by storing that in a (generic) function wrapper. However it will still be rather cumbersome to use, and err on the side of unsafeness as you'll have to match against the ClassTag
/TypeTag
stored inside the wrapper, and cast (either directly through asInstanceOf
or indirectly through the same pattern matching) the function to the corresponding function type.
来源:https://stackoverflow.com/questions/32246734/scala-pattern-match-a-function-how-to-get-around-type-erasure