Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

scala pattern match a function - how to get around type erasure

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

like image 491
Yaneeve Avatar asked Feb 09 '23 05:02

Yaneeve


2 Answers

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
like image 123
Chris Martin Avatar answered Feb 25 '23 04:02

Chris Martin


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.

like image 37
Régis Jean-Gilles Avatar answered Feb 25 '23 04:02

Régis Jean-Gilles