mercredi 13 juillet 2022

Using Scala ClassTags in Collections

I'm trying to create an enum-like type in Scala with a generic type, later doing operations on the instances of the type that depend on what the generic is using Scala's reflect.ClassTag to get information on the generic type. Something like this:

import scala.reflect.ClassTag

sealed trait X[T : ClassTag] {
  def value: T
}

case object I1 extends X[Int] { override def value = 1 }
case object I2 extends X[Int] { override def value = 2 }
case object Sa extends X[String] { override def value = "a" }
case object Sb extends X[String] { override def value = "b" }

val values = IndexedSeq(I1, I2, Sa, Sb)

values.foreach{
  case i: X[Int] => println(s"${i.value} => ${i.value + 1}")
  case s: X[String] => println(s"${s.value} => ${s.value.toUpperCase}")
}

This produces the following warnings:

the type test for Playground.X[Int] cannot be checked at runtime
the type test for Playground.X[String] cannot be checked at runtime

For completeness, when run, it produces the following output (which is reasonable given the warnings):

1 => 2
2 => 3
java.lang.ExceptionInInitializerError
    at Main$.<clinit>(main.scala:24)
    at Main.main(main.scala)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:78)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:568)
    at sbt.Run.invokeMain(Run.scala:143)
    at sbt.Run.execute$1(Run.scala:93)
    at sbt.Run.$anonfun$runWithLoader$5(Run.scala:120)
    at sbt.Run$.executeSuccess(Run.scala:186)
    at sbt.Run.runWithLoader(Run.scala:120)
    at sbt.Run.run(Run.scala:127)
    at com.olegych.scastie.sbtscastie.SbtScastiePlugin$$anon$1.$anonfun$run$1(SbtScastiePlugin.scala:38)
    at scala.runtime.java8.JFunction0$mcV$sp.apply(JFunction0$mcV$sp.java:23)
    at sbt.util.InterfaceUtil$$anon$1.get(InterfaceUtil.scala:17)
    at sbt.ScastieTrapExit$App.run(ScastieTrapExit.scala:259)
    at java.base/java.lang.Thread.run(Thread.java:831)
Caused by: java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Integer (java.lang.String and java.lang.Integer are in module java.base of loader 'bootstrap')
    at scala.runtime.BoxesRunTime.unboxToInt(BoxesRunTime.java:99)
    at Playground$.$anonfun$1(main.scala:17)
    at scala.runtime.function.JProcedure1.apply(JProcedure1.java:15)
    at scala.runtime.function.JProcedure1.apply(JProcedure1.java:10)
    at scala.collection.immutable.Vector.foreach(Vector.scala:1856)
    at Playground$.<clinit>(main.scala:20)
    ... 17 more

I also tried doing it like this, with the singleton objects implemented as instances of case classes instead:

import scala.reflect.ClassTag

sealed trait X[T : ClassTag] {
  def value: T
}

case class I(value: Int) extends X[Int]
case class S(value: String) extends X[String]

val values = IndexedSeq(I(1), I(2), S("a"), S("b"))

values.foreach{
  case i: X[Int] => println(s"${i.value} => ${i.value + 1}")
  case s: X[String] => println(s"${s.value} => ${s.value.toUpperCase}")
}

But I get pretty much the exact same result.

When I do something that appears to me to be similar using a Scala Array, it works:

val values = IndexedSeq(
  Array(1, 2),
  Array(3, 4),
  Array("a", "b"),
  Array("c", "d")
)
values.foreach{
  case i: Array[Int] => println(s"""${i.mkString(",")} => ${i.map(_ * 2).mkString(",")}""")
  case s: Array[String] => println(s"""${s.mkString(",")} => ${s.map(_.toUpperCase).mkString(",")}""")
}

This produces no warnings and the correct output:

1,2 => 2,4
3,4 => 6,8
a,b => A,B
c,d => C,D

What am I doing wrong here? I thought ClassTag was supposed to preserve information about a generic type during runtime? I've seen that reflect.runtime.universe.TypeTag might be better, but the package containing that doesn't seem to be available in Scala 3, and somehow Array is able to do what I want anyway.





Aucun commentaire:

Enregistrer un commentaire