Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Stackable Traits Pattern : method's implementation "needs `abstract override' modifiers"

Tags:

scala

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) ?

like image 665
Francis Toth Avatar asked Sep 10 '15 21:09

Francis Toth


2 Answers

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.

Class Linearization

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.

Abstract override

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.

like image 98
Till Rohrmann Avatar answered Sep 25 '22 16:09

Till Rohrmann


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.

like image 20
Aivean Avatar answered Sep 25 '22 16:09

Aivean