Stackable Traits Pattern: Implementing the "Requires Abstract Override Modifiers" Method

Recently, I found out about a scalable template and follow an example here . Everything works, but there is a case that 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 don’t understand why this does not compile, and why the C :: test method needs the specified modifier.

I noticed that I can make two modifications to make this compilation, either by composing a class C 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 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 we need an additional class (which BTW plays the role of the main class )?

+5
source share
2 answers

The reason for this behavior is Scala linearization of classes, which is used to eliminate the ambiguities and semantics of abstract override . But first, first.

Class linearization

Whenever you have an instance of a type a and you call the method on it a.foobar() , the compiler must figure out where to find the definition of foobar . Since a can extend any other class and feature set, there can be several definitions for the foobar function. To resolve these ambiguities, Scala linearizes your class a with all its superclasses and traits. Linearization will produce the order in which the various types are checked to determine foobar . The first match will be the function being performed.

The Scala specification defines linearization as follows:

Definition 5.1.2. Let C be a class with pattern C1 s ... with Cn {stats}. The linearization of C, L (C) is defined as follows: L (C) = C, L (Cn) +: ... +: L (C1)

Here +: denotes a concatenation where elements of the right operand replace identical elements of the left operand.

Since every theory is gray, 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 redefine the foobar method in class a , because there are several competing definitions for it. However, the question now is which method definition is called by super.foobar . To find out, we must 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 a definition in T1 that returns 1 .

Abstract redefinition

The abstract override modifier for the method basically says that there must be a class / trait I that implements this method, which appears after the attribute with the abstract override modifier in the linearization of the class of your class instance. This is necessary for executing super.foobar() , because super.foobar() entails that in the linearization, a further search is made for the definition of foobar .

When you now look at the definition of class C , you will see that it has the following linearization

  C, B, A 

Therefore, it cannot compile, because starting with B you will not find the test implementation.

When we now look at examples that work, then we will understand why they really 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 the test implementation in C Thus, the code can be compiled. The same is true for the class D example.

+14
source

According to the article you provided :

The base attribute (or abstract class) defines an abstract interface that extends to all cores and flows, as shown in Figure 1. The main features (or classes) implement the abstract methods defined in the base attribute and provide the core core functionality. Each stackable one redefines one or more abstract methods defined in the underlying attribute using Scala abstract redefinition modifiers and provides some behavior and at some point calls a super implementation of the same method. In this way, stackable files change the behavior of any kernel into which they are mixed.

In your case:

  class C extends A with B { override def test = { "C" } } 

you do not have a basic trait. A is basic because it defines an interface, B is stackable (since it calls super , expecting it to be implemented in the kernel), C can also be stacked, since the declaration of test in the class body is the most specific (it overrides one of all signs).

In your "fixed" examples, you simply contributed the correct kernel 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 the kernel.

+1
source

All Articles