I am trying to create an interface where I need, at runtime, the class object corresponding to a generic class.
The following code shows my problem.
package com.example
import java.util.function.Consumer
import scala.reflect.ClassTag
import java.util.Collection
trait DomObj:
def foo(): Unit
trait DomObjA extends DomObj:
def bar(): Unit
/** This is an external, Java API that I cannot change */
trait Store:
def subscribe[T <: DomObj](cls: Class[T], cons: Consumer[Collection[T]]) = ???
/** This is the class I am trying to write */
object Client:
val store: Store = ???
def sub1[T <: DomObj](cons: Consumer[Collection[T]])(using classTag: ClassTag[T]): Unit =
store.subscribe(classTag.runtimeClass, cons)
def sub2[T <: DomObj](cons: Consumer[Collection[T]])(using classTag: ClassTag[T]): Unit =
val cls: Class[T] = classTag.runtimeClass.asSubclass(classOf[DomObj])
store.subscribe(cls, cons)
def sub3[T <: DomObj](cons: Consumer[Collection[T]])(using classTag: ClassTag[T]): Unit =
val cls: Class[T] = classTag.runtimeClass.asSubclass(classOf[T])
store.subscribe(cls, cons)
def subSad[T <: DomObj](cls: Class[T], cons: Consumer[Collection[T]]): Unit =
store.subscribe(cls, cons)
def run(): Unit =
val cons: Consumer[Collection[DomObjA]] = ???
sub1(cons)
sub2(cons)
sub3(cons)
subSad(classOf[DomObjA], cons)
subSad
works, but I would like to avoid needing to explicitly specify the class. This seems reasonable since the compiler knows the class and should be able to pass it as an implicit argument. In fact, that appears to be the purpose of the ClassTag
class.
However, none of the other attempts compiles.
sub1
[error] 30 | store.subscribe(classTag.runtimeClass, cons)
[error] | ^^^^^^^^^^^^^^^^^^^^^
[error] | Found: Class[?]
[error] | Required: Class[T]
[error] |
[error] | where: T is a type in method sub1 with bounds <: com.example.DomObj
sub2
[error] 32 | val cls: Class[T] = classTag.runtimeClass.asSubclass(classOf[DomObj])
[error] | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[error] | Found: Class[? <: com.example.DomObj]
[error] | Required: Class[T]
[error] |
[error] | where: T is a type in method sub2 with bounds <: com.example.DomObj
sub3
[error] 35 | val cls: Class[T] = classTag.runtimeClass.asSubclass(classOf[T])
[error] | ^
[error] | T is not a class type
[error] |
[error] | where: T is a type in method sub3 with bounds <: com.example.DomObj
It appears that in Scala 2, TypeTag
probably did what I wanted, but that does not appear to exist in Scala 3.
Is there a way to make this work, or do I just need to use subSad
?
This does compile:
def sub4[T <: DomObj](cons: Consumer[Collection[T]])(using classTag: ClassTag[T]): Unit =
val cls: Class[T] = classTag.runtimeClass.asInstanceOf[Class[T]]
store.subscribe(cls, cons)
although it uses asInstanceOf
which is often a code smell. It is just putting type parameters back on which we know are correct.
The cast you mention is because the runtime class doesn't necessarily correspond to the Scala type. The T
in ClassTag[T]
is the Scala type, while the U
in Class[U]
is the erased Java-side class. While these two types usually coincide (modulo erasure of generic parameters), there are examples where they do not.
So casting the Class[_]
to a Class[T]
is fair game. It's an unchecked cast, but unchecked casts on Class[_]
are not uncommon, even in the Java world. While I certainly respect your caution on asInstanceOf
, it's pretty okay in this case.