Collections of Traits with types generics in Scala 2.10

488 views Asked by At

I'm trying to build collections of objects, defined at run time and using type generics, by referring to their superclass (well, trait), but I'm having a tough time transforming them back into the child objects. Some example code and results:

trait MyTrait[T] {
  def f(x: T): Int
}

class Foo extends MyTrait[Double] {
  def f(x: Double) = 1
}

class Bar extends MyTrait[String] {
  def f(x: String) = 2
}

val fooInstance: Foo = new Foo
val barInstance: Bar = new Bar
val myTraitList: List[MyTrait[_]] = List(fooInstance, barInstance)

println(fooInstance.getClass)
// prints "class MyExample$Foo"
println(barInstance.getClass)
// prints "class MyExample$Bar"

println(myTraitList(0).getClass)
// prints "class MyExample$Foo", so the list is preserving object classes and not just "MyTrait[_]"
println(myTraitList(1).getClass)
// prints "class MyExample$Bar", again, preserving object class

println(fooInstance.f(1.0))
// prints "1"
println(barInstance.f("blah"))
// prints "2"

println(myTraitList(0).f(1.0))
// this is where things break:
// "type mismatch; found : Double(1.0) required: _$3 where type _$3"
// so, the list element knows it's an instance of Foo (as indicated by getClass), but has lost the overloaded definition of f

println(myTraitList(1).f("blah"))
// the same error occurs on the Bar instance:
// "type mismatch; found : String("blah") required: _$3 where type _$3"

If I hard-code a myTraitList(0).asInstanceOf[Foo].f(1.0), it (predictably) works just fine. So, I tried to create a function to do that at run time:

def castToCorrectChildClass(o: MyTrait[_], label: Char): MyTrait[_] = {
  return label match {
    case 'f' => o.asInstanceOf[Foo]
    case 'b' => o.asInstanceOf[Bar]
    case _ => o
  }
}

Unfortunately, this is subject to the same problems:

println(castToCorrectChildClass(myTraitList(0), 'f').f(1.0))
// type mismatch; found : Double(1.0) required: _$2 where type _$2

One solution is to make a List[MyTrait[Any]] to store the instances and make the type parameter covariant: trait MyTrait[+T]. Unfortunately, in my real-world code, I need it to be invariant for other reasons, so I can't use this workaround. I've also tried using ClassTags and TypeTags to remember the child class thinking this were a reflection-related problem, but I didn't have any luck (and I suspect that that's not the intended use case for these, anyway, though perhaps I'm mistaken).

Any suggestions? I'd love to have a flexible collection of unknown number (so, no Tuples) of child objects extending the same trait but with different types as per user input gathered at run time, and I'm happy to accept the trade-off of doing bookkeeping to ensure I don't mis-cast (or mis-asInstanceOf) an object back to the incorrect type, but I can't seem to get it working. Thanks in advance!

1

There are 1 answers

6
marios On

I am not exactly sure what you want to do, but the problem you are facing is because the type of your myTraitList is similar to having a List[MyTrait[Any]]. Since the type is _ the compiler doesn't know if it's Bar or Foo. Thankfully Scala provides Pattern Matching that allows you do condition on the type of an object.

Solution 1

def conditionalDo(x: MyTrait[_]) = {
    x match {
        case i: Foo => println("doing foo() stuff")
        case j: Bar => println("doing bar() stuff")
        case _ => println("error!")
    }
}

myTraitList.foreach(conditionalDo)

Solution 2

trait MyTrait {
  def f(x: Any): Int
}

class Foo extends MyTrait {
  def f(x: Any) = {
    x match {
       case i: Int => 1
       case _ => -1
    }
  }
}

class Bar extends MyTrait {
  def f(x: Any) = {
    x match {
       case j: String => 2
       case _ => -1
    }
  }
}

val fooInstance: Foo = new Foo
val barInstance: Bar = new Bar

val myTraitList: List[MyTrait] = List(fooInstance, barInstance)
myTraitList(0).f(1) // returns 1
myTraitList(1).f("s") // returns 2