Search code examples
scalaoverridingtype-aliaspath-dependent-type

Incompatible type error when overriding type bounds


Can't figure out why scalac is unhappy here (2.12):

trait A {
  type Self <: A
  type X <: Self
}

trait B extends A {
  override type Self <: B
  override type X = C // error: overriding type X in trait A with bounds <: B.this.Self
}

trait C extends B {
  override type Self = C
}

Feels like it is because of the path-dependent types, but I don't understand what exactly is wrong and if there's a good way to fix it.


Solution

  • C is a subtype of B, B is a subtype of A, so C is a subtype of A but C is not a subtype of A's Self or B's Self. So you can't override (in B) A's X having upper bound Self (i.e. A's Self) with C not satisfying the bound (i.e. B's Self).

    trait A {
      type Self <: A
      type X <: Self
    
      // implicitly[C <:< Self] // doesn't compile
    }
    
    trait B extends A {
      override type Self <: B
      // override type X = C 
    
      // implicitly[C <:< Self] // doesn't compile
    }
    
    trait C extends B {
      override type Self = C
    }
    

    C's Self equals C but this doesn't mean that A's Self or B's Self does.

    You can fix compilation with lower bound

    trait A {
      type Self <: A
      type X <: Self
    }
    
    trait B extends A {
      override type Self >: C <: B // >: C is added
      override type X = C 
    }
    
    trait C extends B {
      override type Self = C
    }
    

    Or if you mean that A's X is a subtype of not A's Self but C's Self you can specify this with type projection

    trait A {
      type Self <: A
      type X <: C#Self // here
    }
    
    trait B extends A {
      override type Self <: B
      override type X = C
    }
    
    trait C extends B {
      override type Self = C
    }
    

    I guess misunderstanding was because for defs

    trait A {
      def foo(): String = "A#foo()"
      def bar(): String = s"bar=A#bar(), foo=${foo()}"
    }
    
    trait B extends A {
      def foo(): String = "A#foo()"
    }
    
    trait C extends B {
      override def foo(): String = "C#foo()"
    }
    

    when we write foo() inside A's bar() we refer actually not to A's foo() but to implementation's foo(). This is possible because method implementations are resolved late, at runtime. But types are resolved early, at compile time. So when you write

    trait A {
      type Self <: A
      type X <: Self
    }
    

    Self in the upper bound of X is A's Self, not implementation's Self.

    OOP principles say that inside A you can't refer specifically to C's foo() (unless you instantiate C). But you can everywhere refer specifically to A's Self, B's Self, C's Self with type projections A#Self, B#Self, C#Self.