0

Here is a simple example:


object MatchErasedType {

  trait Supe {
    self: Singleton =>

    type T1
    lazy val default: T1

    def process(v: Any): T1 = {
      v match {
        case vv: T1 => vv
        case _ => default
      }
    }
  }
}

It will throw a compiler warning:

MatchErasedType.scala:13:14: the type test for Supe.this.T1 cannot be checked at runtime

The case is interesting as any instance of Supe is guaranteed to be a Singleton. So a delayed reification of process won't have any erasure. As a result, this question actually entails 2 different cases:

  • How to eliminate it if all instances of Supe are specialised Singleton, presumably without using any implicit summoning or conversion?

  • How to eliminate it in other cases?

UPDATE 1: It should be noted that the type of v: Any cannot be known in compile-time, the call-site won't be able to provide such information. Henceforth, This question is NOT for cases when T1 cannot be resolved to a concrete class and/or runtime condition.

tribbloid
  • 4,026
  • 14
  • 64
  • 103
  • 4
    `given Typeable[T1]` next to `type T1` is supposed to be a solution to this issue - https://docs.scala-lang.org/scala3/reference/other-new-features/type-test.html - if it doesn't work then I would complain to compiler team on GitHub. – Mateusz Kubuszok Feb 11 '23 at 22:48
  • @MateuszKubuszok thanks a lot, will try it for the non-singleton case. Still may need a specialised alternative for the other case tho – tribbloid Feb 11 '23 at 23:48
  • You'd have to specify how you intend to use `Singleton` - if there should be only one you can do just an equality check against this value rather than type check. And if this is Scala's singleton then you could use `given [T <: Singleton : ValueOf]: Typeable[T] with /* here summon[ValueOf[T]] and compare references against matched object */` – Mateusz Kubuszok Feb 12 '23 at 00:11
  • `Singleton` is already in that example, and if every `process()` is specialised for every singleton instance, no `Typable` will ever be needed. – tribbloid Feb 12 '23 at 04:48

1 Answers1

1

In Scala 2

trait Supe { self: Singleton =>
  type T1 <: Singleton
  def default: T1

  def process(v: Any): T1 =
    v match {
      case vv: T1 => println(1); vv
      case _      => println(2); default
    }
}

object Impl1 extends Supe {
  override type T1 = Impl1.type
  override lazy val default: T1 = this
}

object Impl2 extends Supe {
  override type T1 = Impl2.type
  override lazy val default: T1 = this
}

Impl1.process(Impl1) // 1
Impl1.process(Impl2) // 1

To overcome type erasure, in Scala 2 we use scala.reflect.ClassTag. Either (approach 1)

trait Supe { self: Singleton =>
  type T1 <: Singleton
  def default: T1

  def process(v: Any)(implicit ct: ClassTag[T1]): T1 =
    v match {
      case vv: T1 => println(1); vv
      case _      => println(2); default
    }
}

object Impl1 extends Supe {
  override type T1 = Impl1.type
  override lazy val default: T1 = this
}

object Impl2 extends Supe {
  override type T1 = Impl2.type
  override lazy val default: T1 = this
}

Impl1.process(Impl1) // 1
Impl1.process(Impl2) // 2

or (approach 2)

trait Supe { self: Singleton =>
  type T1 <: Singleton
  def default: T1

  implicit def ctag: ClassTag[T1]

  def process(v: Any): T1 =
    v match {
      case vv: T1 => println(1); vv
      case _      => println(2); default
    }
}

object Impl1 extends Supe {
  override type T1 = Impl1.type
  override lazy val default: T1 = this
  override implicit def ctag: ClassTag[T1] = {
    val ctag = null // hiding implicit by name, otherwise implicitly[...] returns the above ctag
    implicitly[ClassTag[T1]]
  }
}

object Impl2 extends Supe {
  override type T1 = Impl2.type
  override lazy val default: T1 = this
  override implicit def ctag: ClassTag[T1] = {
    val ctag = null // hiding implicit by name, otherwise implicitly[...] returns the above ctag
    implicitly[ClassTag[T1]]
  }
}

Impl1.process(Impl1) // 1
Impl1.process(Impl2) // 2

Similarly, in Scala 3

trait Supe { self: Singleton =>
  type T1 <: Singleton
  lazy val default: T1

  def process(v: Any): T1 =
    v match {
      case vv: T1 => println(1); vv
      case _      => println(2); default
    }
}

object Impl1 extends Supe {
  override type T1 = Impl1.type
  override lazy val default: T1 = this
}

object Impl2 extends Supe {
  override type T1 = Impl2.type
  override lazy val default: T1 = this
}

Impl1.process(Impl1) // 1
Impl1.process(Impl2) // 1

In Scala 3 scala.reflect.Typeable (scala.reflect.TypeTest) is used instead of ClassTag. The approach 1 works

trait Supe { self: Singleton =>
  type T1 <: Singleton
  lazy val default: T1

  def process(v: Any)(using Typeable[T1]): T1 =
    v match {
      case vv: T1 => println(1); vv
      case _      => println(2); default
    }
}

object Impl1 extends Supe {
  override type T1 = Impl1.type
  override lazy val default: T1 = this
}

object Impl2 extends Supe {
  override type T1 = Impl2.type
  override lazy val default: T1 = this
}

Impl1.process(Impl1) // 1
Impl1.process(Impl2) // 2

Regarding the approach 2, in Scala 3 there is no way to hide implicit by name, so let's put it into a local scope so that it will not be found in object implicit scope

trait Supe { self: Singleton =>
  type T1 <: Singleton
  lazy val default: T1

  def tt: Typeable[T1]

  def process(v: Any): T1 = {
    given Typeable[T1] = tt

    v match {
      case vv: T1 => println(1); vv
      case _      => println(2); default
    }
  }
}

object Impl1 extends Supe {
  override type T1 = Impl1.type
  override lazy val default: T1 = this
  override def tt: Typeable[T1] = summon
}

object Impl2 extends Supe {
  override type T1 = Impl2.type
  override lazy val default: T1 = this
  override def tt: Typeable[T1] = summon
}

Impl1.process(Impl1) // 1
Impl1.process(Impl2) // 2

I still think given or using could be avoided in all instances. Either method requires a lot of boilerplate, particularly if the pattern matching contains a lot of union or intersection types

In your last example, adding every new case using types like T1 with Int or T1 {def v: Int} necessitates a Typeable. This is infeasible in many cases.

ClassTag, TypeTag, shapeless.Typeable/TypeCase (Scala 2), TypeTest/Typeable, Shapeless-3 Typeable (Scala 3) are standard tools to overcome type erasure. Matching types at runtime is not typical for pattern matching. If your business logic is based on types then maybe you don't need pattern matching at all, maybe type classes would be a better choice

trait Supe:
  self: Singleton =>

  type T1 <: Singleton
  lazy val default: T1

  trait Process[A]:
    def process(a: A): T1

  trait LowPriorityProcess:
    given low[A]: Process[A] with
      def process(a: A): T1 = { println(2); default }
  object Process extends LowPriorityProcess:
    given [A <: T1]: Process[A] with
      def process(a: A): T1 = { println(1); a }

  def process[A: Process](v: A): T1 =
    summon[Process[A]].process(v)

object Impl1 extends Supe:
  override type T1 = Impl1.type
  override lazy val default: T1 = this

object Impl2 extends Supe:
  override type T1 = Impl2.type
  override lazy val default: T1 = this

Impl1.process(Impl1) // 1
Impl1.process(Impl2) // 2

or

trait Process[A, T1 <: Singleton]:
  def process(a: A, default: T1): T1

trait LowPriorityProcess:
  given low[A, T1 <: Singleton]: Process[A, T1] with
    def process(a: A, default: T1): T1 =
      {println(2); default}
object Process extends LowPriorityProcess:
  given[A <: T1, T1 <: Singleton]: Process[A, T1] with
    def process(a: A, default: T1): T1 =
      {println(1); a }

trait Supe:
  self: Singleton =>

  type T1 <: Singleton
  lazy val default: T1

  def process[A](v: A)(using p: Process[A, T1]): T1 =
    p.process(v, default)

object Impl1 extends Supe:
  override type T1 = Impl1.type
  override lazy val default: T1 = this

object Impl2 extends Supe:
  override type T1 = Impl2.type
  override lazy val default: T1 = this

Impl1.process(Impl1) // 1
Impl1.process(Impl2) // 2

adding every new case using types like T1 with Int or T1 {def v: Int} necessitates a Typeable. This is infeasible in many cases.

trait Supe:
  self: Singleton =>

  type T1 <: Singleton
  lazy val default: T1

  def tt[S <: T1]: Typeable[S]

  def process(v: Any): T1 =
    given Typeable[T1] = tt
    given tt1: Typeable[T1 with SomeTrait] = tt
    given tt2: Typeable[T1 {def v: Int}] = tt
    //...

    v match
      case vv: T1 => {println(1); vv}
      case _      => {println(2); default}

trait SomeTrait

object Impl1 extends Supe:
  override type T1 = Impl1.type with SomeTrait
  override lazy val default: T1 = this.asInstanceOf[T1]
  override def tt[S <: T1]: Typeable[S] = summon[Typeable[S @unchecked]]

object Impl2 extends Supe:
  override type T1 = Impl2.type {def v: Int}
  override lazy val default: T1 = this.asInstanceOf[T1]
  override def tt[S <: T1]: Typeable[S] = summon[Typeable[S @unchecked]]

Impl1.process(Impl1) // 1
Impl1.process(Impl2) // 2
Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • Is there a reason to go with approach 2 over 1? And can you explain a bit more about why the `"hiding implicit by name..."` piece needs to happen? – anqit Feb 12 '23 at 17:10
  • 2
    @anqit *"Is there a reason to go with approach 2 over 1?"* For example if you can't modify the signature of the method (can't add implicit parameter). `trait Supe {...} extends SupeSupe` `trait SupeSupe { type T1 <: Singleton; def process(v: Any): T1}` (`SupeSupe` can be third-party trait). – Dmytro Mitin Feb 12 '23 at 17:14
  • 1
    @anqit *"why the "hiding implicit by name..." piece needs to happen?"* You can check that if you don't hide then the code silently compiles but at runtime you'll get `StackOverflowError` https://scastie.scala-lang.org/DmytroMitin/BLq4IOGqRTiHI86XzhJVIQ/1 – Dmytro Mitin Feb 12 '23 at 17:22
  • @anqit Without hiding `override implicit def ctag: ClassTag[T1] = implicitly[ClassTag[T1]]` is just `override implicit def ctag: ClassTag[T1] = ctag` i.e. infinite recursion. The thing is that without hiding, during defining `override implicit def ctag...`, inside its body this `implicit def ctag` is seen and `implicitly...` is resolved as this implicit to be defined. – Dmytro Mitin Feb 12 '23 at 17:26
  • @anqit https://stackoverflow.com/questions/61917884/nullpointerexception-on-implicit-resolution https://stackoverflow.com/questions/64286575/extending-an-object-with-a-trait-which-needs-implicit-member https://stackoverflow.com/questions/73712378/is-there-a-workaround-for-this-format-parameter-in-scala – Dmytro Mitin Feb 12 '23 at 17:33
  • 1
    @anqit That's the reason why it's better to switch on `-Wself-implicit`/`-Ywarn-self-implicit` https://docs.scala-lang.org/overviews/compiler-options/index.html – Dmytro Mitin Feb 12 '23 at 17:38
  • Thanks a lot professor, I still think `given` or `using` could be avoided in all instances. Either method requires a lot of boilerplate, particularly if the pattern matching contains a lot of union or intersection types – tribbloid Feb 13 '23 at 03:49
  • In your last example, adding every new case using types like `T1 with Int` or `T1 {def v: Int}` necessitates a Typeable. This is infeasible in many cases. – tribbloid Feb 13 '23 at 03:51
  • @DmytroMitin Thanks a lot for all the alternative solutions, I've updated my question to narrow down the solution I'm looking for – tribbloid Feb 13 '23 at 20:05
  • @tribbloid *"adding every new case using types like `T1 with Int` or `T1 {def v: Int}` necessitates a `Typeable`"* Just in case, in `given/using` approach I guess this can be handled with `def tt[S <: T1]: Typeable[S]`. See the update. – Dmytro Mitin Feb 14 '23 at 08:39