Skip to content

ClassCastException when using Union type with case object and TypeTest #22950

Open
@GuillaumeSaintRaymondSonos

Description

Compiler version

3.6.4

Minimized code

I'm building a toy typeclass (named Typeclass) and I want it to support Union types, so I wrote a combine method that takes a Typeclass[A] and a Typeclass[B] and returns a Typeclass[A | B]. I'm using TypeTest to be able to match the type at compile time.
When declaring a Typeclass instance for a case object, this sample code compiles correctly but generates a ClassCastException.

import scala.reflect.TypeTest

trait Typeclass[A] {
  def print(a: A): String
}

object Typeclass {
  def apply[A: Typeclass]: Typeclass[A] = implicitly

  def instance[A](fn: A => String): Typeclass[A] = fn(_)

  def combine[A, B](
      typeclassA: Typeclass[A],
      typeclassB: Typeclass[B]
  )(using
      TypeTest[A | B, A],
      TypeTest[A | B, B]
  ): Typeclass[A | B] =
    Typeclass.instance[A | B](
      {
        case a: A => typeclassA.print(a)
        case b: B => typeclassB.print(b)
      }
    )
}

case class CaseClassA(value: String = "A")
case class CaseClassB(value: String = "B")
case object CaseObject

val typeclass: Typeclass[CaseClassA | CaseClassB | CaseObject.type] =
  Typeclass.combine(
    Typeclass.instance[CaseClassA](_.value),
    Typeclass.combine(
      Typeclass.instance[CaseObject.type](_ => "C"),
      Typeclass.instance[CaseClassB](_.value)
    )
  )

def print(elem: CaseClassA | CaseClassB | CaseObject.type) =
  println(typeclass.print(elem))

print(CaseClassA())
print(CaseClassB())
print(CaseObject)

Output

By running scala-cli -S 3.6.4 classCastException.sc, I get:

A
B
Exception in thread "main" java.lang.ClassCastException: class classCastException$_$CaseObject$ cannot be cast to class classCastException$_$CaseClassB (classCastException$_$CaseObject$ and classCastException$_$CaseClassB are in unnamed module of loader 'app')
        at classCastException$_.$init$$$anonfun$7(classCastException.sc:38)
        at classCastException$_.classCastException$_$Typeclass$$$_$combine$$anonfun$1(classCastException.sc:22)
        at classCastException$_.classCastException$_$Typeclass$$$_$instance$$anonfun$1(classCastException.sc:10)
        at classCastException$_.print(classCastException.sc:41)
        at classCastException$_.<init>(classCastException.sc:45)
        at classCastException_sc$.script$lzyINIT1(classCastException.sc:60)
        at classCastException_sc$.script(classCastException.sc:60)
        at classCastException_sc$.main(classCastException.sc:64)
        at classCastException_sc.main(classCastException.sc)

Expectation

A
B
C

Workaround

If a declare typeclass by switching CaseClassA and CaseObject typeclass instances, there's no exception and it gives the correct result

val typeclass: Typeclass[CaseClassA | CaseClassB | CaseObject.type] =
  Typeclass.combine(
    Typeclass.instance[CaseObject.type](_ => "C"),
    Typeclass.combine(
      Typeclass.instance[CaseClassA](_.value),
      Typeclass.instance[CaseClassB](_.value)
    )
  )

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:union-typesIssues tied to union types.itype:bugitype:soundnessSoundness bug (it lets us compile code that crashes at runtime with a ClassCastException)

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions