10

For the code below as I understand it linearization of expression

new E with D with C with B

is E -> C -> B -> D. So then shouldnt the expression d.foo() in the code

below evaluate to ECBD instead of CBDE. What am I missing?

    trait A {
      def foo(): String = ""
    }

    trait B extends A {
      abstract override def foo() = "B" + super.foo()
    }

    trait C extends B {
      abstract override def foo() = "C" + super.foo()
    }

    trait D extends A {
      abstract override def foo() = "D" + super.foo()
    }

    class E extends A{
      override def foo() = "E"
    }

    var d = new E with D with C with B;
    d.foo() //prints CBDE

I have noticed that if I have a class F like below

class F extends A with D with C with B{
      override def foo() = "F" + super.foo()
}

and do

new F().foo

it prints "FCBD"

It seems a bit inconsistent to me because class F is mixed in the same way as the expression but has a different print order

2 Answers 2

6

The first case with new E with D with C with B is perfectly explained here. Its linearization is EDBC, so when you call d.foo(), it

  • first calls C#foo(),
  • then B#foo(),
  • then D#foo()
  • and finally E#foo().

If you make E a trait and mix it in the end: val d = new D with C with B with E, then d.foo() will return just "E", because trait E is the "last" in the linearization and just overridesfoo.

The case of F is different, because you define foo as "F" + super.foo(), and super in this case is A with D with C with B whose linearization is ADBC, so new F().foo() - first prints "F", - then its super.foo() which is "CBD".

By the way, try changing A#foo() to return "A", then you will see that in E you override A's foo so "A" doesn't appear in the result, and in F it is "FCBDA".

2
  • Just for further clarification, was it intentional that both D and B extended A?
    – ssj_100
    Commented Dec 11, 2019 at 15:57
  • I think yes, it forms the "diamond" shaped graph which is the cause of complexity for linearization here. Commented Dec 11, 2019 at 18:21
1

So let's add some lines that show us instantiation order

  trait A {
    print("A")
    def foo(): String = "A"
  }

  trait B extends A {
    print("B")
    abstract override def foo() = "B" + super.foo()
  }

  trait C extends B {
    print("C")
    abstract override def foo() = "C" + super.foo()
  }

  trait D extends A {
    print("D")
    abstract override def foo() = "D" + super.foo()
  }

  class E extends A {
    print("E")
    override def foo() = "E" + super.foo()
  }

  var e = new E with D with C with B
  println()
  println(s"e >> ${e.foo()}")

printed: AEDBC e >> CBDEA

but what with F?

  class F extends A with D with C with B {
    print("F")
    override def foo() = "F" + super.foo()
  }
  val f = new F()
  println()
  println(s"f >> ${f.foo()}")

printed: ADBCF f >> FCBDA

As You can see the linearization for both cases different! When we instantiate the class with a cortege of traits it's not the same as when we create a separate class that inherits those traits.

So the order of calling foo different too, according to linearization. And it's a little bit clear when we add super.foo() to the E

Not the answer you're looking for? Browse other questions tagged or ask your own question.