Dispatching on generic type without object

Consider the following pseudo code:


sealed trait JasmArgT { }
case class JasmArg_Int (v: Int) extends JasmArgT {}
case class JasmArg_Float (v: Float) extends JasmArgT {}

object JasmArgUtil {
  def toDescriptor[T <: JasmArgT]: String = {
    case JasmArg_Int => "I"
    case JasmArg_Float => "F"
  }
}

Note that the “toDescriptor” method does NOT have an argument of type JasmArgT. It also does NOT have an argument of type T. It has no arguments, on this generic parameter “T”.

Question: Is there some way to dispatch on this generic T (known at compile time) so that:

toDescriptor[JasmArg_Int] => "I"
toDescriptor[JasmArg_Float] => "f"

?

Context: We are doing some dynamic code generation. We store the output type of a piece of dynamically generated code block. We know the type of the output, but we don’t have a object of the type. We need to calculate this “descriptor” as part of code generation.

1 Like

A general way is to use a TypeTag and match on this.

regards,
Siddhartha

ClassTag should suffice for this case. That one is less powerful than TypeTag and doesn’t require any extra dependencies.

import scala.reflect.{classTag, ClassTag}

object JasmArgUtil {
  def toDescriptor[T <: JasmArgT: ClassTag]: String = {
    val clazz = classTag[T].runtimeClass
    if (classOf[JasmArg_Int].isAssignableFrom(clazz)) "I" 
    else if (classOf[JasmArg_Float].isAssignableFrom(clazz)) "F" 
    else throw new MatchError(clazz)
  }
}
scala> JasmArgUtil.toDescriptor[JasmArg_Int]
val res33: String = I

scala> JasmArgUtil.toDescriptor[JasmArg_Float]
val res34: String = F

scala> JasmArgUtil.toDescriptor[JasmArgT]
scala.MatchError: interface JasmArgT (of class java.lang.Class)
  at JasmArgUtil$.toDescriptor(<console>:6)
  ... 40 elided

In Scala 3 it’s even easier:

scala> object JasmArgUtil {                                                     
     |   inline def toDescriptor[T <: JasmArgT]: String = inline erasedValue[T] match {
     |     case _: JasmArg_Int => "I"
     |     case _: JasmArg_Float => "F"
     |   }
     | }
// defined object JasmArgUtil

scala> JasmArgUtil.toDescriptor[JasmArg_Int]
val res0: String = I

scala> JasmArgUtil.toDescriptor[JasmArg_Float]                                  
val res1: String = F

scala> JasmArgUtil.toDescriptor[JasmArgT]                                  
1 |JasmArgUtil.toDescriptor[JasmArgT]
  |^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |cannot reduce inline match with
  | scrutinee:  scala.compiletime.erasedValue[JasmArgT] : JasmArgT
  | patterns :  case _:JasmArg_Int
  |             case _:JasmArg_Float
  | This location contains code that was inlined from rs$line$5:2

Also note that in this case you get the error at compile time instead of runtime, and you don’t even need any context bounds. You can probably achieve something similar in Scala 2 but that would require a macro.

3 Likes

The general mechanism in scala to materialize terms based on types are implicits.

The TypeTag/ClassTag approaches shown here work, but are a bit awkward in matching on them.

The following is a bit less awkward when matching, but more awkward when declaring:

sealed trait JasmArgT { }
case class JasmArg_Int (v: Int) extends JasmArgT {}
case class JasmArg_Float (v: Float) extends JasmArgT {}

object JasmArgUtil {
  class JasmArgTagger[A](val tag: String)
  implicit val tInt: JasmArgTagger[JasmArg_Int] = new JasmArgTagger("I")
  implicit val tFloat: JasmArgTagger[JasmArg_Float] = new JasmArgTagger("F")
  def toDescriptor[T <: JasmArgT](implicit tagger: JasmArgTagger[T]): String = tagger.tag
}

It’s got that whole OOP visitor pattern vibe going on.

A third option us to move the logic to the method itself and do pattern matching in the implicit vals:

object JasmArgUtil {
  class JasmArgTag[A]()
  implicit val JasmArg_Int: JasmArgTag[JasmArg_Int] = new JasmArgTag()
  implicit val JasmArg_Float: JasmArgTag[JasmArg_Float] = new JasmArgTag()
  def toDescriptor[T <: JasmArgT](implicit tag: JasmArgTag[T]): String = tag match {
    case JasmArg_Int => "I"
    case JasmArg_Float => "F"
  }
}

Anyway, praised be scala 3 where you can get around all the awkwardness.

3 Likes