Scala has a wealth of language constructs that support modern object-oriented programming. Class Linearization stands in the heart of its object model.
Some languages like Java opt for single inheritance and interfaces to provide some functionalities.
class ButtonClass extends WidgetClass
implements ColorInterface, EffectInterface
An obvious shortcoming of this approach is that since no implementations are allowed in interfaces, the deriving class has to implement everything which is against code reuse. Java offers abstract class that could have implementation, but at the end of day it's still a class hence is restricted to be extended only.
Class Linearization flattens types in a DAG to a linear hierarchy. I'll try to visualize what it means. The syntax of trait and class roughly looks like this:
trait T [extends (C | T) (with T)*] [body]
class C [extends (C | T) (with T)*] [body]
The construct/initialize order follows the linearized hierarchy from top to bottom. E.g. in the figure, C1, T2, C2, T3, T1, then CA.
The mixed class in which trait/class A mixes is everything from the top util A in the linearized hierarchy. E.g. T3 mixes in "C1 >: T2 >: C2".
When trait A extends class/trait B, it implies that the mixed class in which we mix A must be a subclass of B. To put it another way, B must appear somewhere above A in the linearized hierarchy. Therefore, trying the example of CB would give the following error.
scala> class CB extends C2 with T1 with T3 with T4
<console>:14: error: illegal inheritance; superclass C2
is not a subclass of the superclass C3
of the mixin trait T4
In addition, if we see trait A { self: B => ...}:
trait B { val bar = "B" }
trait A { self: B =>
val foo = self.bar
}
It implies the concrete class must be a subclass of B. That is, B must appear anywhere in the linearized hierarchy. We should be careful about object construction/initialization. Becase if B appears below A in the linearized hierarchy, A's initialization that reads uninitialized B's feilds would get nothing.
// init/construct order: B -> A -> $anon$
scala> val c = new B with A
c: B with A = $anon$1@2efe33c7
scala> c.foo
res0: String = B
scala> c.bar
res1: String = B
// init/construct order: A -> B -> $anon$
scala> val d = new A with B
c: A with B = $anon$1@9904c66
scala> d.foo
res2: String = null // *nothing*
scala> d.bar
res3: String = B
Lastly, Scala also gives us abstract override. If I abstract override T1's "bar" in "trait T2 extends T1", the mixed class in which T2 mixes must have overriden this method too and I'll need it in my version of "bar".
trait T1 {
def foo: String = "T1 "
def bar: String
}
trait T2 extends T1 {
// 'super.foo' can be safely accessed since it already has a "default" definition in T1(which could be overriden by the mixed class).
override def foo = "T2 " + super.foo
// 'super.bar' has not defined from T2's perspective. Adding "abstract" to make sure the mixed class must have defined it.
abstract override def bar = "T2 " + super.bar
}
class C1 extends T1 {
override def foo = "C1 " + super.foo
def bar = "C1 "
}
// T1 >: C1 >: T2 >: $anon$
scala> val c1 = new C1 with T2
c1: C1 with T2 = $anon$1@6e39dcd1
scala> c1.foo
res0: String = "T2 C1 T1 "
scala> c1.bar
res1: String = "T2 C1 "
That's what I undertand so far about Scala's Class Linearization.