Should tagged types work for value classes?

98 Views Asked by At

I am trying to create a tagged type (in Scala 2.x) 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:

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)
  }
}

My question is whether this should be considered a Scala compiler bug/improvement or not?

I haven't looked at the Java bytecode but from the JS side it seems relatively easy to solve.

0

There are 0 best solutions below