TypeTest to overcome erasure with generic types in scala3

Viewed 395

I am having problems understanding how TypeTests in scala3 can replace the use of TypeTag in scala 2. The usecase being able to match on a generic parameter like x: List[Int].

Concrete example I am trying to solve:

enum Foo :
  case Bar()
  case Baz()

case class Mod[T <: Foo](modFn: T => T)

def modifyBarsOrBaz(mod: Mod[_]) = mod match
  case barMod: Mod[Foo.Bar] => ???
  case bazMod: Mod[Foo.Baz] => ???

the compilation results (as expected) in the comiler warning(s)

the type test for Mod[Foo.Bar] cannot be checked at runtime and an unreachable case.

Now my question is: is this possible to do at all in scala3?

I was under the impression that I would somehow have to provide a TypeTest[Any, Mod[Foo.X]] for all X which are subclasses of the Foo enum.

But I am struggling to even implement those tests, as well as understanding what using parameter the modifyBarsOrBaz is required for this to work.

Thus I came up with the following (not working) solution:

def modifyBarsOrBaz[T <: Foo](mod: Mod[T])(using TypeTest[Any, Mod[T]]) = mod match
  case barMod: Mod[Foo.Bar] => ???
  case bazMod: Mod[Foo.Baz] => ???

and a naive tt implementation as such

val tt: TypeTest[Any, Mod[Foo.Bar]] =
  new TypeTest[Any, Mod[Foo.Bar]] :
    def unapply(x: Any): Option[x.type & Mod[Foo.Bar]] = x match
      case m: Mod[_] => ??? // what to do here? use a classtag on Mod?

I tried to search the web for answers, but since this is pretty new I was not lucky. Any hints?

1 Answers

The problem here is that a TypeTest[Any, Mod[T]] will be able to check if an Any is a Mod[T], not to check whether or not a Mod[T] is a Mod[Foo.Bar] or a Mod[Foo.Baz]. What you would need are TypeTest[Any, Mod[Foo.Bar] and TypeTest[Any, Mod[Foo.Baz]:

def modifyBarsOrBaz(mod: Mod[?])(using asBar: TypeTest[Any, Mod[Foo.Bar]], asBaz: TypeTest[Any, Mod[Foo.Baz]]) =
  mod match
    case asBar(barMod) => println("barMod")
    case asBaz(bazMod) => println("bazMod")

I don't know why a simple barMod: Mod[Foo.Bar] didn't work with a TypeTest[Any, Mod[Foo.Bar]] in scope, I'll come to that later.

However, you now have to actually implement those TypeTests yourself. Since the JVM doesn't have reification, you will have to store information about T in the Mod class. If you really want to keep Mod a single case class, you can do this:

type BarOrBaz[T <: Foo] <: String = T match {
  case Foo.Bar => "Bar"
  case Foo.Baz => "Baz"
}

case class Mod[T <: Foo](modFn: T => T, tag: BarOrBaz[T])

TypeTest instances for Bar and Baz can now be provided with an inline method:

import compiletime.constValue

inline given [T <: Foo]: TypeTest[Mod[?], Mod[T]] = new TypeTest:
  def unapply(mod: Mod[?]) = Option.when(mod.tag == constValue[BarOrBaz[T]])(mod.asInstanceOf[mod.type & Mod[T]])

modifyBarsOrBaz would be

def modifyBarsOrBaz(mod: Mod[?])(using asBar: TypeTest[Mod[Foo], Mod[Foo.Bar]], asBaz: TypeTest[Mod[Foo], Mod[Foo.Baz]]) = mod match
  case asBar(barMod) => println("barMod")
  case asBaz(bazMod) => println("bazMod")

For convenience, an inline apply method could be made:

object Mod:
  inline def apply[T <: Foo](modFn: T => T) = new Mod(modFn, constValue[BarOrBaz[T]])

And you could use it like this (Scastie):

modifyBarsOrBaz(Mod[Foo.Bar](bar => bar))  //barMod
modifyBarsOrBaz(Mod[Foo.Baz](baz => baz))  //bazMod

But really, the need for type tests such as this feels like a code smell to me. I'd suggest rethinking your design to work around it rather than use tags like this.

Related