I ran into some strange situation in Scala today while I tried to refine the type bounds on an abstract type member.
I have two traits that define bounds on a type member and combine them in a concrete class. That works fine but when matching / casting with the trait combination only one of the two TypeBounds is "active" and I struggle to understand why ...
I tried to prepare an example:
trait L
trait R
trait Left {
type T <: L
def get: T
}
trait Right {
type T <: R
}
now if I combine these two traits in one concrete class
val concrete = new Left with Right {
override type T = L with R
override def get: T = new L with R {}
}
I can access my member via get as intended
// works fine
val x1: L with R = concrete.get
but if I cast to (Left with Right) or pattern match I cannot access the member anymore. Dependent on the order I get either the type bounds from Left or from Right but not the combination of both.
// can only access as R, L with R won't work
val x2: R = concrete.asInstanceOf[Left with Right].get
// can only access as L, L with R won' work
val x3: L = concrete.asInstanceOf[Right with Left].get
I understand that Left with Right is not the same thing as Right with Left but in both cases both type bounds are included, so why can I only get one to work?
can anyone shed some light on why this is happening?
the second type member overrides the first one.
trait L
trait R
trait Left {
type T <: L
def get: T
}
trait Right {
type T <: R
}
object X {
type LR = Left with Right // Right#T overrides Left#T, LR#T is now <: R
type RL = Right with Left // Left#T overrides Right#T, LR#T is now <: L
val concrete = new Left with Right {
override type T = L with R
override def get: T = new L with R {}
}
// ok
val r: R = concrete.asInstanceOf[LR].get
val l: L = concrete.asInstanceOf[RL].get
// ok
implicitly[LR#T <:< R]
implicitly[RL#T <:< L]
// doesn't compile, LR#T is a subclass of R because Right#T overrides Left#T
implicitly[LR#T <:< L]
// doesn't compile, RL#T is a subclass of L because Left#T overrides Right#T
implicitly[RL#T <:< R]
}
In "concrete" you override the type member with L with R
, but when you cast it to Left with Right
you lose that refinement, and T becomes _ <: L or _ <: R depending on the order of the traits.
Since type members can be overridden, if you upcast (e.g. to LR or RL) you lose the refinement you applied in concrete. You could say concrete is at the same time a RL and a LR, but when you upcast it to LR or RL you lose the information you have in the other one
Additionally to TrustNoOne's answer I can suggest following workaround with some limitations. You can design your own type combinator instead of with
to overcome type overriding.
trait L
trait R
trait Base {
type T
def get: T
}
trait Nil extends Base{
type T = Any
}
trait ~+~[X[_ <: Base] <: Base, Y[_ <: Base] <: Base] extends Base {
type T = Y[X[Nil]]#T
}
trait Left[B <: Base] extends Base {
type T = B#T with L
}
trait Right[B <: Base] extends Base {
type T = B#T with R
}
val concrete = new (Left ~+~ Right) {
def get: T = new L with R {}
}
val x1: L with R = concrete.get
val x2: R = concrete.asInstanceOf[Left ~+~ Right].get
val x3: L = concrete.asInstanceOf[Right ~+~ Left].get
This code now compiles successfully, note however that we can not merge namespaces of combined types in new type, so all known methods should be defined in Base
or derived in some typeclass via mechanic similar to shapeless HList
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With