Reputation: 3770
I have a code that tries to wrap a function in another that does dynamic type checking:
class Base
class Foo extends Base
class Bar extends Base
object Main{
def checker[A <: Base]( func : A => String) : Base => String =
(b : Base) => b match {
case a : A => func(a)
case _ => "error"
}
def fooFunc(f : Foo) = "It's a foo"
def main(arg : Array[String]) {
val check = checker(fooFunc)
println(check(new Foo) + ", " + check(new Bar))
}
}
This produces the following error:
Exception in thread "main" java.lang.ClassCastException: Bar cannot be cast to Foo
at Main$$anonfun$1.apply(Main.scala:17)
at Main$.main(Main.scala:19)
at Main.main(Main.scala)
If I remove the type parameter and replace A with Foo in the definition of checker, it works well. However, if I keep the type parameter but omit the function argument and replace func(a) with "good", I get "good" for both Foo and Bar.
Is this what is called type erasure? I'm not well familiar with the concept..
Also, I'd love to hear a solution around this.
Upvotes: 4
Views: 1494
Reputation: 1934
Yes, you found yourself in the land of erasure.
In the 1st case (the original code), the compiler knows that A
is Foo
, but at runtime the type parameter is erased to the upper type bound, which is Base
in this example (if you don't specify an upper type bound, the type parameter is erased to Object
). So JVM sees your code as follows (notice no type parameterization):
def checker(func: Base => String): Base => String =
(b: Base) => b match {
case a : Base => func(a)
case _ => "error"
}
Any Foo
or Bar
object will match Base
and then JVM will try to cast it to Foo
and call func
. Works if b
is an object of class Foo
, but throws cast exception for Bar
. No surprises.
In the 2nd case you remove the type parameter and replace A
with Foo
in the definition of checker
. So your code at runtime looks like this (the function is not type parametrized to begin with, so nothing is erased):
def checker(func: Foo => String): Base => String =
(b: Base) => b match {
case a: Foo => func(a)
case _ => "error"
}
This works as expected, but it's fixed to checking only for Foo
. So you'll need to write a separate checker
for Bar
and any other type you want to test for.
In the 3rd case you keep the type parameter but omit the function argument and replace func(a)
with "good"
. This results in a similar code to case 1 (again erasure from Foo
to Base
), with the exception that func
is not called so the cast to Foo
is not needed. Hence, no exception.
def checker: Base => String =
(b: Base) => b match {
case a: Base => "good"
case _ => "error"
}
Simply any object you pass (subclass of Base) is matched by the first clause and checker
always returns '"good"'.
Now the solution. You were on the right track with Manifest
(but the code you showed is way too complicated for what you're trying to achieve). When you ask for a Manifest
Scala compiler passes an additional object to a method/function that can be used at runtime to 'recover' the erased type(s). Below I use 'context bound' instead of spelling it out as (implicit manifest : Manifest[A])
, but it's the same, just shorter.
def checker[A <: Base: Manifest](func: A => String): Base => String =
(b: Base) => if(manifest[A].erasure == b.getClass) func(b.asInstanceOf[A])
else "error"
So then when you call it like this:
def fooFunc(f: Foo) = "It's a foo"
def barFunc(f: Bar) = "It's a bar"
def main(arg: Array[String]) {
val check1 = checker(fooFunc)
val check2 = checker(barFunc)
println(check1(new Foo) + ", " + check1(new Bar))
println(check2(new Foo) + ", " + check2(new Bar))
}
You'll get the output as expected:
It's a foo, error
error, It's a bar
Erasure is a source of all kinds of fun in Scala since type parameterization here is more common than in Java. No way around it, I recommend learning everything about it you can.
Upvotes: 2
Reputation: 3770
I found a way using manifests.
class Base
class Foo extends Base
class Bar extends Base
trait Functor[A] {
def apply[B](b : B)(implicit mb : Manifest[B]) : A
}
case class Checker[A](func : A => String)(implicit manifest : Manifest[A]) extends Functor[String]{
def apply[B](b : B)(implicit mb : Manifest[B]) = {
if (mb == manifest) func(b.asInstanceOf[A])
else "error"
}
}
object Main{
def fooFunc(f : Foo) = "good"
def main(arg : Array[String]) {
val check = Checker(fooFunc)
println(check(new Foo) + ", " + check(new Bar))
}
}
I'd still like to hear suggestions from someone who knows what they're doing though.
Upvotes: 3
Reputation: 11237
as you have it defined, checker can only accept a function that takes a Foo.
If you make fooFunc generic as well, then it should work:
def fooFunc[A <: Base](f : A) = "It's a foo"
but then fooFunc would not be an appropriate name since it could return anything that derives from Base.
def baseFunc[A <: Base](f : A) = "It's a "+f.getClass
might be what you're looking for
EDIT
class Base
class Foo extends Base
class Bar extends Base
def checker[A <: Base]( func : A => String) : Base => String =
(b : Base) => b match {
case a : A => func(a)
case _ => "error"
}
def fooFunc[A <: Base](f : A) = "It's a "+f.getClass.getName
val check = checker(fooFunc)
println(check(new Foo) + ", " + check(new Bar))
Upvotes: 1