Karolis Juodelė
Karolis Juodelė

Reputation: 3770

ClassCastException in a higher order generic function

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

Answers (3)

romusz
romusz

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

Karolis Juodelė
Karolis Juodelė

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

virtualeyes
virtualeyes

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

Related Questions