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

2

4

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

Autocade answered 27/8, 2015 at 10:19 Comment(1)
Would shapeless help? (https://mcmap.net/q/266238/-how-to-pattern-match-on-generic-type-in-scala)Mainsheet
D
4

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
Dispel answered 27/8, 2015 at 11:30 Comment(2)
The problem with this way of using TypeTag is that you have to preserve the exact type all along. Say that you have a list of functions stored in a list, and all of a the compiler has no idea what the exact types of the functions are, and you calling foo on them will not do what you expect. So you are still severely limited in terms of pattern matching, compared to what you could do if erasure did not enter the picture. If you pack the TypeTag with the function itself (inside a wrapper), then you can discriminate the actual type in your patterns.Karlee
Updated to demonstrate using a wrapper.Dispel
T
4

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.

Tubby answered 27/8, 2015 at 10:43 Comment(2)
Thanks, that helps clarify a little bit, but you still don't pose a solution, or, conversely say there isn't one...Autocade
I expanded my answer.Karlee
D
4

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
Dispel answered 27/8, 2015 at 11:30 Comment(2)
The problem with this way of using TypeTag is that you have to preserve the exact type all along. Say that you have a list of functions stored in a list, and all of a the compiler has no idea what the exact types of the functions are, and you calling foo on them will not do what you expect. So you are still severely limited in terms of pattern matching, compared to what you could do if erasure did not enter the picture. If you pack the TypeTag with the function itself (inside a wrapper), then you can discriminate the actual type in your patterns.Karlee
Updated to demonstrate using a wrapper.Dispel

© 2022 - 2024 — McMap. All rights reserved.