Reputation: 3838
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.
Upvotes: 0
Views: 328
Reputation: 51658
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
orusing
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 typesIn your last example, adding every new case using types like
T1 with Int
orT1 {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
orT1 {def v: Int}
necessitates aTypeable
. 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
Upvotes: 1