I am trying to create a tagged type in the same way that Shapeless (2.x) does but it fails with a ClassCastException
:
object Main {
sealed trait TaggedProps[Props] extends Any
final class KeyAddingStage(private val args: Array[Any]) extends AnyVal
type RenderedProps[Props] = KeyAddingStage with TaggedProps[Props]
object Name {
case class Props(name: String)
def component(props: Props): KeyAddingStage = new KeyAddingStage(
Array(
props
)
)
def apply(name: String): RenderedProps[Props] = component(Props(name)).asInstanceOf[RenderedProps[Props]]
}
def main(args: Array[String]): Unit = {
val first: RenderedProps[Name.Props] = Name("Jason")
val firstStage: KeyAddingStage = first
println(first == firstStage)
val props: Seq[RenderedProps[Name.Props]] = Seq(first)
val propsHead: RenderedProps[Name.Props] = props.head
println(firstStage == propsHead)
}
}
Fails with:
[error] java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to Main$KeyAddingStage
[error] at Main$.main(test.scala:26)
[error] at Main.main(test.scala)
[error] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
[error] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
[error] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
[error] at java.lang.reflect.Method.invoke(Method.java:498)
I initially found this in a Scala.js project and I thought that it was a Scala.js bug but it does in fact fail on the JVM too.
I found a workaround which is to do it in a similar way to scala-newtype but with a subtle disctinction (Support @newsubtype for value classes · Issue #73 · estatico/scala-newtype · GitHub):
import scala.language.implicitConversions
object Main {
final class KeyAddingStage(private val args: Array[Any]) extends AnyVal
object KeyAddingStage {
implicit def build(stage: KeyAddingStage): String = stage.toString
}
type RenderedProps[Props] = RenderedProps.Type[Props]
object RenderedProps {
// This must be a structural type like it is in @newtype
type Base = {
type __RenderedProps__newtype
}
trait Tag[Props] extends Any
// But we can put the type we are extending here after Base so that it works in the same way as @newsubtype
type Type[Props] <: Base with KeyAddingStage with Tag[Props]
def apply[Props](stage: KeyAddingStage): RenderedProps[Props] = stage.asInstanceOf[RenderedProps[Props]]
}
object Name {
case class Props(name: String)
def component(props: Props): KeyAddingStage = new KeyAddingStage(
Array(
props.asInstanceOf[Any]
)
)
def apply(name: String): RenderedProps[Props] = RenderedProps(component(Props(name)))
}
def main(args: Array[String]): Unit = {
val first: RenderedProps[Name.Props] = Name("Jason")
val firstStage: KeyAddingStage = first
val str: String = first
println(first == firstStage)
val props: Seq[RenderedProps[Name.Props]] = Seq(first)
val propsHead: RenderedProps[Name.Props] = props.head
println(firstStage == propsHead)
}
}
There is a long standing issue in Shapeless that seems related to this: Runtime exception if the value of record field is Any or AnyRef · Issue #44 · milessabin/shapeless · GitHub
My question is whether this should be considered a Scala compiler bug/improvement or not?
The same thing happens in Scala 3 FWIW.
I ran it through javap
and the structural refinement type causes it to be treated as java/lang/Object
and all the checkcast
ops are gone which is a great trick but feels like it ought to have better support.
I originally asked this on StackOverflow: scala - Should tagged types work for value classes? - Stack Overflow