scala

Get fully qualified class object from Scala generic parameters


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.


Solution

  • 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.