I don't understand why the following scala code doesn't compile:
sealed trait A
case class B() extends A {
def funcB: B = this
}
case class C() extends A {
def funcC: C = this
}
def f[T <: A](s:T): T = s match {
case s: B => s.funcB
case s: C => s.funcC
}
It works to replace f with
def f[T <: A](s:T): A = s match {
case s: B => s.funcB
case s: C => s.funcC
}
and then cast to the subtype when f is called, using asInstanceOf, for example. But I would like to be able to construct a function which unifies some previously defined methods, and have them be type stable. Can anyone please explain?
Also, note that the following f also compiles:
def f[T <: A](s:T): T = s match {
case s: B => s
case s: C => s
}
What makes it work?
In particular, in Scala 3 you could use match types
In general, for the solution to "return current type" problems see Scala FAQ How can a method in a superclass return a value of the “current” type?
Compile-time techniques such as type classes and match types can be considered as kind of compile-time pattern matching which instruct the compiler to reduce to the most specific informationally rich type used at call site instead of otherwise having to determine a probably poorer upper bound type.
Why it does not work?
The key concept to understand is that parametric polymorphism is a kind of universal quantification which means it must make sense to the compiler for all instantiations of type parameters at call-sites. Consider typing specification
which the compiler might interpret something like so
hence the expression
exprrepresenting the body offmust type to that particular
T. Now lets try to type ourexprThe type of
is
B, and the type ofis
C. Given we haveBandC, now compiler has to take the least upper bound of the two which isA. ButAis certainly not alwaysT. Hence the typecheck fails.Now lets do the same exercise with
This specification means (and observe the "for all" again)
Now lets type the method body expressions
As before we arrive at types
BandC, so compiler takes the upper bound which is the supertypeA. And indeed this is the very return type we specified. So typecheck succeeds. However despite succeeding, at compile-time we lost some typing information as compiler will no longer consider all the information that comes with specificTpassed in at call-site but only the information available via its supertypeA. For example, ifThas a member not existing inA, then we will not be able to call it.What to avoid?
Regarding
asInstanceOf, this is us telling the compiler to stop helping us because we will take the rains. Two groups of people tend to use it in Scala to make things work, the mad scientist library authors and ones transitioning from other more dynamically typed languages. However in most application level code it is considered bad practice.