Recently, I've find out about the stackable trait pattern and followed the example described here. Everything works, but there is a case I cannot understand :
trait A {
def test : String
}
trait B extends A {
// 'abstract override' modifier required as
// the test() method is not yet implemented
abstract override def test = {
s"B${super.test}"
}
}
class C extends A with B {
// test method concrete implementation
override def test = { "C" }
}
<console>:10: error: overriding method test in trait B of type => String;
method test needs `abstract override' modifiers
class C extends A with B { override def test = { "C" } }
I cannot understand why this does not compile, and why the C::test method needs the mentioned modifier.
I've noticed that there is two modifications I can do in order to make this compile, either by composing the C class at runtime :
class C extends A { override def test = { "C" } }
new C with B // works as expected
or by adding an extra class (which is kind of the same but at compile time):
class C extends A {
override def test = { "C" }
}
class D extends C with B
new D().test
res5: String = BC
Why do I need an extra class (which BTW plays the role of the Basic class) ?
The reason for this behaviour is Scala's class linearization which is used to resolve ambiguities and the semantics of abstract override
. But first things first.
Whenever you have an instance a
of type A
and you call a method on it a.foobar()
, the compiler has to figure out where to find the definition of foobar
. Since A
can extend any other class and a set of traits, there might be multiple definitions for the function foobar
. In order to resolve these ambiguities, Scala will linearize your class A
with all its superclasses and traits. The linearization will produce an order in which the different types are checked for a definition of foobar
. The first match will be the function which is executed.
The Scala specification defines the linearization as following
Definition 5.1.2 Let C be a class with template C1 with ... with Cn { stats }. The linearization of C, L(C) is defined as follows: L(C) = C , L(Cn)+: ... +: L(C1)
Here +: denotes concatenation where elements of the right operand replace identical elements of the left operand.
Since all theory is grey, let's take a look at an example:
trait T1 {
def foobar() = 1
}
trait T2 {
def foobar() = 2
}
class B extends T2 {
override def foobar() = 42
}
class A extends B with T1 with T2 {
override def foobar() = super.foobar()
}
First of all, we have to override the foobar
method in the class A
, because we have multiple competing definitions for it. However, now is the question, which method definition is called by super.foobar
. In order to find this out, we have to calculate the linearization of A
.
L(A) = A, L(T2) +: L(T1) +: L(B)
L(B) = B, L(T2)
L(T2) = T2
L(T1) = T1
L(A) = A, T2 +: (T1, B, T2)
L(A) = A, T1, B, T2
Thus, super.foobar
will call the definition in T1
which returns 1
.
The abstract override
modifier for a method basically says that there has to be a class/trait I
implementing this method which appears after the trait with the abstract override
modifier in the class linearization of your instantiated class. That is necessary in order to execute super.foobar()
, because super.foobar()
entails that the linearization is further searched for a definition of foobar
.
When you now look at your definition of class C
then you'll see that it has the following linearization
C, B, A
Consequently, it cannot compile, because beginning from B
you don't find an implementation of test
.
When we now look at the examples which work, then we'll why they actually work. In the case of C extends A
with new C with B
, you basically create an anonymous class Z extends C with B
. The linearization of Z
is
Z, B, C, A
There you see, that B
can find in C
an implementation of test
. Thus, the code can compile. The same holds true for the example with class D
.
According to the article you provided:
The base trait (or abstract class) defines an abstract interface that all the cores and stackables extend, as shown in Figure 1. The core traits (or classes) implement the abstract methods defined in the base trait, and provide basic, core functionality. Each stackable overrides one or more of the abstract methods defined in the base trait, using Scala's abstract override modifiers, and provides some behavior and at some point invokes the super implementation of the same method. In this manner, the stackables modify the behavior of whatever core they are mixed into.
In you case:
class C extends A with B { override def test = { "C" } }
you don't have core trait. A
is base, as it defines the interface, B
is stackable (as it calls super
, expecting it to be implemented in core), C
is also stackable, as the test
declaration in the body of the class is the most concrete (it overrides one from all of the traits).
In your "fixed" examples you just introduced correct core implementation:
class C extends A { override def test = { "C" } }
new C with B // works as expected
class C extends A {
override def test = { "C" }
}
class D extends C with B
Here C
defines test
before it is overridden by B
, so it serves as core.
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