Scala has a wealth of language constructs that support modern object-oriented programming. Class Linearization stands in the heart of its object model. Section 12.6 in Artima's Programming Scala is an invaluable resource. Section 7.5 in Oreilly's Programming Scala also provides a succint explaination, though its full content is not freely available online. In this article, I will try to explain what Class Linearization is and how to make it speak for our design rather than have it get in the way.
Let's start with Multiple Inheritance. Whether MI is bad is an unsettled debate(for example: http://programmers.stackexchange.com/questions/218458/is-there-any-real-reason-multiple-inheritance-is-hated). Some languages like Java opt for single inheritance and interfaces to provide some functionalities of MI(http://en.wikipedia.org/wiki/Multiple_inheritance). E.g.
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.
The desire of having interfaces that contain implementation leads us to trait(another solution is mixin http://stackoverflow.com/questions/925609/mixins-vs-traits). Class Linearization is really the magic behind Scala's object model that weaves classes and traits altogether. A little bit pedantic description:
...inheritance hierarchy forms a directed, acyclic graph (see [ScalaSpec2009]). The term linearization refers to the algorithm used to "flatten" this graph for the purposes of resolving method lookup priorities, constructor invocation order, binding of super, etc.
~ Programming Scala
In short, 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. Please take a pinch of salt and feel free to point out any incorrect information.