erip erip - 1 month ago 10
Scala Question

How can I parameterize a class with an object's type to get an instance of it in Scala?

I'd like to parameterize a class with the type of an object to make my code more generic. By doing this, I don't need an implementation for all objects that extend a certain trait.

I have the following code that demonstrates my goal:

abstract trait Foo {
def greet
}

object Coo extends Foo {
def greet = println("Coo!")
}

object Boo extends Foo {
def greet = println("Boo!")
}

class A[Fooer <: Foo] {
def moo = asInstanceOf[Fooer].greet()
}

object Main extends App {
new A[Coo.type].moo() // hopefully prints "Coo!"
}


The code is throwing an exception:

java.lang.ClassCastException: A cannot be cast to Foo


And I believe it's because of the
asInstanceOf
call which is seemingly using
this
implicitly.

Essentially my question is this: how can I get an instance of my object by type within a class via parameterization?

Answer

And I believe it's because of the asInstanceOf call which is seemingly using this implicitly.

Every class in Scala extends Any, which inherits the special method asInstanceOf, among other things. I wouldn't say it's happening implicitly--it's just that you're calling a member method of that class, which is calling it on itself. Same as if you wrote def moo = greet.

Furthermore, any time you use asInstanceOf, you're asking the compiler to lie to itself and keep going. This will more often than not result in a ClassCastException. In this case, it's because you're attempting to cast an instance A into some unknown sub-type of Foo. This will never work. In the very specific case, you're attempting to pass the singleton type Coo.type to an instance of A and then create a new instance of Coo.type. While there are many other reasons why this won't work, one of them is that you can't create a second instance of a singleton type--it's called that for a reason.

More generally, you don't know that you can simply construct an object given a type. A type doesn't have a constructor, a class does.

This might be a side-effect of being a simplified example, but you're concerned with creating an instance of A with some type parameter without needing to pass it an actual object, but the objects you're concerned with already exist. So why not pass them?

class A[Fooer <: Foo](f: Fooer) {
  def moo = f.greet
}

The only other way to do this is to provide evidence that Fooer can be constructed. There are no type constraints for proving something is a constructable class, though. What you can do is require a Class meta object instead:

class A[Fooer <: Foo](clazz: Class[Fooer]) {
  def moo = clazz.newInstance.greet
}

This is crude, however. First, it doesn't work with your singleton types above (because they can't be constructed again). And second, we can only call newInstance when there are no constructor parameters to provide, and there's no way to enforce that.

So while this will work:

scala> class Bar extends Foo { def greet = print("Bar") }

scala> new A(classOf[Bar]).moo
Bar

The following will not:

scala> class Baz(i: Int) extends Foo { def greet = println("Baz") }
defined class Baz

scala> new A(classOf[Baz]).moo
java.lang.InstantiationException: Baz

scala> new A(classOf[Coo.type]).moo
<console>:14: error: class type required but Coo.type found
       new A(classOf[Coo.type]).moo
                        ^

Alternatively, you might use a type class that allows you to consistently build instances of Foo, but that would require creating one for each sub-class. e.g.:

trait FooBuilder[A] {
    def build(): A
}

implicit object BarBuilder extends FooBuilder[Bar] {
    def build() = new Bar
}

class A[Fooer <: Foo](implicit fb: FooBuilder[Fooer]) {
    def moo = fb.build().greet
}