Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scala Abstract type members - inheritance and type bounds

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?

like image 266
Michael Zinsmaier Avatar asked Dec 11 '22 18:12

Michael Zinsmaier


2 Answers

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

like image 134
Giovanni Caporaletti Avatar answered Jan 06 '23 21:01

Giovanni Caporaletti


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

like image 36
Odomontois Avatar answered Jan 06 '23 20:01

Odomontois