Contents
- Overview
- Trait Declarations
- Trait Class Files
- Linearization Rules
- Class Initialization
- Method Overriding
- Variable Overriding
- Type Overriding
- Glossary
Overview
Unlike Java interfaces, Scala traits can include code, which effectively gives the ability to do multiple inheritance. Implementations of multiple inheritance without linearization can suffer from the diamond inheritance problem, in which there is an ambiguity in how to deal with an attribute such as a method which could be inherited from either parent. Linearization specifies a single linear order for all of the ancestors of a class, including both the regular superclass chain and the parent chains of all of the traits.Scala traits that contain code are called mixin (or mix-in) traits. If you don't use any mixin traits, then the only code is in the regular superclass chain, just as in Java, and linearization is not an issue. Since linearization is only of interest when using mixin traits, it is worth reviewing the characteristics and constraints on such traits.
Trait Declarations
A Scala trait can include code, in which case the trait is called a mixin trait. The code can be any of the following:- method definitions.
- mutable and immutable variables (vars and vals).
- a no-argument constructor (a trait may not have a constructor with parameters).
- Both can use
with
clauses to inherit from additional traits (with some restrictions, see below). - Every class and trait declaration is always implicitly extended
to include
with ScalaObject
at the end. If you explicitly addwith ScalaObject
you will get an error that it has been inherited twice. - If a class or trait does not explicitly extend any class or trait, then its superclass is AnyRef, which compiles to java.lang.Object (just as in Java).
- If a class or trait is declared to extend a trait directly rather than
extending a class
with
that trait, that declaration is treated the same as if it explicitly extended the trait's superclasswith
the trait. - Every user-defined class and trait
has exactly one superclass that it extends, which is one of
- the explicitly extended superclass (as opposed to extending a trait),
- the superclass of the trait being explicitly extended, or
- implicitly AnyRef when nothing is explicitly extended.
All class and trait declarations can be converted into a canonical form in which the superclass (a class, not a trait) is explicitly specified using
extends
,
all traits are specified using with
,
and the ScalaObject
trait is automatically appended to the end.
For example, given these trait and class declarations:
the canonical form for each of these is:class A class B extends A trait C trait D extends B class E extends D with C
The following class definitions all produce an identical class file:class A extends AnyRef with ScalaObject class B extends A with ScalaObject trait C extends AnyRef with ScalaObject trait D extends B with ScalaObject class E extends B with D with C with ScalaObject
class A class A extends AnyRef class A extends java.lang.Object
Trait Class Files
When you define a Scala trait with only method declarations but no code, Scala produces a Java interface class file, just as you would get by defining an interface in Java. When you include code in your Scala trait, Scala still produces the same Java interface class file, but it also produces a second class file that contains your code. For example, create this simple Scala fileA.scala
:
Compile withtrait A { def a:Int }
scalac A.scala
to produce A.class
,
then dump it with javap -c A
to get this:
Now modify the trait to include some code:Compiled from "A.scala" public interface A{ public abstract int a(); }
Compile withtrait A { def a:Int = 1 }
scalac
and you will see two class files:
in addition to the interface class file A.class
there is a code class file called A$class.class
.
Running javap -c A
shows that
A.class
is identical to what it was when A.scala
contained no code.
Running javap -c 'A$class'
shows the added code:
TheCompiled from "A.scala" public abstract class A$class extends java.lang.Object{ public static void $init$(A); Code: 0: return public static int a(A); Code: 0: iconst_1 1: ireturn }
a
method is our code that returns 1,
and the $init$
method is our empty constructor code.
When a class extends a trait, any variables declared in the trait appear in the class, methods in the trait get turned into facade methods in the class that turn around and call the code for that method in the trait's code class, and the constructor for the class makes a call to the
$init$
method in the trait's code class.
If you want to see this in more detail, you can create a class B
that extends trait A, compile it, and run javap on the B.class file.
The output is a bit convoluted, which is why I tried to summarize
here what is going on.
Linearization Rules
Scala's linearization rules are described starting on page 49 of the Scala Language Specification (SLS), Chapter 5, "Classes and Objects".In order to allow reuse of compiled classes and to ensure well-defined behavior, the linearization must satisfy a few rules:
- The linearization of any class must include unmodified the linearization of any class (but not trait) it extends.
- The linearization of any class must include all classes and mixin traits in the linearization of any trait it extends, but the mixin traits need not be in the same order as they appear in the linearization of the traits being mixed in.
- No class or trait may appear more than once in the linearization.
The disallowance of disjoint class ancestry constrains the allowable combinations of trait and class inheritance. Thus while it is possible for a class to inherit from a trait, and for a trait to inherit from a class, not all traits can be used with all classes. A trait whose superclass is AnyRef can be mixed in with any class, but if the trait's superclass is something more specific, then there are some classes with which that trait can not be used. Given a class C that is extending a superclass S, the only traits that can be mixed in with C are traits whose superclass is either S or an ancestor of S.
From the way the Java language and VM works, we know that when an object is initialized, the constructor for the
java.lang.Object
class runs first.
In terms of linearization, this means that Object
must
be at one end of the linearization,
and that initialization must start at that end.
By convention,
Scala linearizations are listed from left to right,
with the rightmost class being the most general, i.e. Object
.
The combination of Object
(which in Scala translates to
AnyRef
, or in a linearization AnyRef
followed by Any
) being the rightmost class together
with the rule that the linearization of a class must include the
linearization of its superclass means that the linearization of
the superclass must appear as a suffix (i.e. as the rightmost part)
of the linearization of the class.
As mentioned earlier, all class and trait declarations are implicitly extended by adding
with ScalaObject
at the end.
The predefined Scala classes such as Any and AnyRef do not include
this declaration.
Thus if you declare a class
the linearization of that class will be (SLS section 5.1.2):class Foo extends AnyRef
If you now declare a class that extends that class{ Foo, ScalaObject, AnyRef, Any }
the linearization of the extended class will include the linearization of the superclass as a suffix:class Bar extends Foo
The linearization of the values classes (such as Int in this example) are all of the form{Bar, Foo, ScalaObject, AnyRef, Any }
Since these classes are final and you can't extend them, you don't need to worry about calculating linearizations.Int, AnyValue, Any
The linearization of a reference class is calculated using the following algorithm:
- Start with the class declaration, for example:
class C extends S with T1 with T2
- Reverse the order of the list, except keep the first item (C)
at the beginning, and drop the other keywords:
C T2 T1 S
- Replace each item in the list except the first (C) with its linearization:
C T2L T1L SL
- Insert a right-associative list-concatenation operator between each
element in the list:
C +: T2L +: T1L +: SL
- Append the standard Scala classes
ScalaObject, AnyRef, Any
:C +: T2L +: T1L +: SL +: ScalaObject +: AnyRef +: Any - Evaluate the list to get the final linearization. The operator works on two lists as follows: remove any items from the left hand list that appear in the right hand list, then prepend the remaining items from the left hand list to the right hand list (if either side is not a list, treat it as a list with one element). The operator is right-associative, so start with the classes on the right end and work your way to the left until all lists have been combined; in this example, the first possibility to remove anything will be to look at SL and remove any duplicates of ScalaObject, AnyRef and Any; then look at T1L and remove anything already in SL or to the right of it, and so on back to C.
The SLS gives as Example 5.1.3 this set of class declarations and the linearization of class
Iter
:
For another description of the linearization algorithm, including some more complex examples, see Linearization of an Object's Hierarchy in Chapter 7, The Scala Object System, in the 2008 O'Reilly book Programming Scala by Dean Wampler and and Alex Payne.abstract class AbsIterator extends AnyRef { ... } trait RichIterator extends AbsIterator { ... } class StringIterator extends AbsIterator { ... } class Iter extends StringIterator with RichIterator { ... } { Iter, RichIterator, StringIterator, AbsIterator, ScalaObject, AnyRef, Any }
Class Initialization
When creating an instance of a class (what the SLS calls "template evaluation", section 5.1), the constructor code is executed according to the order of classes in the linearization but in reverse, from right to left. The first constructors executed are forAny
and
AnyRef
,
and the last is for the class being instantiated.
Because the linearization is defined to include as a suffix the linearization of the superclass, this means the entire constructor of the superclass is executed before the constructor of the class or any of its mixin traits are executed.
After the superclass constructor is executed, the constructors for each mixin trait are executed. Since they are executed in right-to-left order within the linearization, but the linearization is created by reversing the order of the traits, this means the constructors for the mixin traits are executed in the order that they appear in the declaration for the class. Remember, however, that when mixins share hierarchy, the order of execution may not be quite the same as how the mixins appear in the declaration.
Finally, the constructor for the class being instantiated is executed. This happens after the constructors for all superclasses and mixins have been executed.
Method Overriding
As in Java, when class A extends class B in Scala it can typically override method definitions in class B. The SLS has a detailed set of rules (section 5.1.4) about when an extending class can override a member (def, var or val) of a supertype. These are approximately:- You can't override a final or private.
- Visibility (access modifiers such as public or protected) and laziness must match.
- In a concrete class, all abstracts must be overridden.
- When overriding non-abstract members, the
override
keyword must be used.
super
as an
object qualifier before the method name.
In Scala, you can invoke the supertype using the same syntax,
or you directly reference any of the traits in the declaration of
the class, possibly skipping some of the overridden methods of
the traits between,
by qualifying the super
keyword with a trait type
(a trait qualifier, to form a static super reference;
SLS, section 6.5, page 73).
Consider the following scala definitions, along with a simple test object T, which you can compile and run to print the values indicated by the comments:
The above code shows how you can specify a particular parent trait to call. You can directly call to any of the traits in theclass A { def t = 1 } trait B extends A { override def t = super.t * 2 } trait C extends A { override def t = super.t * 3 } class D1 extends B with C { override def t = super.t } class D2 extends B with C { override def t = super[B].t } class D3 extends B with C { override def t = super[C].t } class E1 extends C with B { override def t = super.t } class E2 extends C with B { override def t = super[B].t } class E3 extends C with B { override def t = super[C].t } object T { def main(args:Array[String]) { println((new D1).t) //prints 6 println((new D2).t) //prints 2 println((new D3).t) //prints 6 println((new E1).t) //prints 6 println((new E2).t) //prints 6 println((new E3).t) //prints 3 } }
with
clause,
or to the direct supertype being extended, but you can not directly call
supertypes of your direct supertype.
Note that traits B and C both extend A, so you might think that calling
super.t
from within either B or C would refer to A.t
,
but that's not how Scala works.
The super.t
reference is to the next class in the
linearization, working from left to right along that list.
In the linearization of D1, C comes before B (D1, C, B, A),
so calling super.t
from C calls to B.t
,
and calling super.t
from B calls to A.t
;
but the linearization of E1 has B before C (E1, B, C, A),
so the super.t
calls between B and C are in the other direction,
with super.t
in B calling C.t
and
super.t
in C calling A.t
.
Given that you can compile B and C separately, then compile the D and E classes using just the class files for B and C rather than their source files, how is it that Scala can make
super.t
in B call
A.t
in one case and C.t
in another?
If you run javap
on the B and B$class classes, you will
see the answer:
Scala creates a method$ javap B Compiled from "T.scala" public interface B extends scala.ScalaObject{ public abstract int t(); public abstract int B$$super$t(); } $ javap 'B$class' Compiled from "T.scala" public abstract class B$class extends java.lang.Object{ public static void $init$(B); public static int t(B); }
B$$super$t
that is defined in the
B interface, but not implemented in the B$class class.
When the trait is used in a class declaration, as in D1 or E1,
Scala creates an implementation of B$$super$t
that
calls the appropriate super method.
Variable Overriding
You can override variables (vals or vars) using the same rules as for methods. In particular, the "laziness" of the overriding variable must match that of the overridden variable: either they are both lazy, or neither is lazy.The
lazy
declaration is particularly useful when setting
up variable overrides,
since the order of execution of initialization code
can make it difficult to understand what is happening.
For example, consider this code in a query posted to the Scala listserv by Sébastien Bocq in July of this year:
When B is instantiated it throws a NoSuchElementException in line 2 of class A. Despite the initialization of the overriding h in class B, the initialization of h in class A still occurs, since it is part of the class initialization of A, which is executed before anything in class B is executed.abstract class A(s:Option[String]) { val h = s.get } class B extends A(None) { override val h = "Hello" }
One easy solution, as Sébastien points out, is to make h a
lazy
val in both classes.
Unlike methods, you can not use the
super
notation with
variables.
When you override a variable, the overridden version is no longer available.
However, note that you can override a def
in a superclass
with a val
in an extending class:
When overriding aclass A { def x = 1 } class B extends A { override val x = 2 }
def
with a val
you
can access the def
in the superclass by using
the super
notation,
which will call that method once when calculating the initialization of
the val
:
class B extends A { override val x = super.x + 1 }
Type Overriding
Scala currently uses linearization to resolve type overrides in the same way as method and variable overrides, by using inheritance with linearization. This is sometimes not what people expect, and Martin has commented that he has considered changing this behavior to make overridden type declarations compositional and commutative, which might make them more useful and less surprising, despite the fact that overriding types would then be different from overriding other elements.Scala types have a lot of flexibility, and when you mix that flexibility with overriding you can get into complicated situations and unexpected behavior pretty quickly. There is enough more to say about types to make another entire post, so I will not go into that in any more detail here.
I leave you with this valid Scala code fragment, which I find interesting:
class P { def x=1; def y=2 } trait X { type T <: { def x:Int } } trait Y { type T <: { def y:Int } } class C extends X with Y { type T = P }
Glossary
- base classes: "The classes reachable through transitive closure of the direct inheritance relation from a class C" (SLS, section 5.1.2, page 52). In other words, all of the immediate supertypes and their supertypes back to the root object.
- least proper supertype of a class: "the class type or compound type consisting of all its parent class types" (SLS, section 5.1, page 50).
- linearization: the arranging of a class and its base classes into a linear ordering.
- parent class or type: one of the classes or traits listed in the declaration of a class or trait from which it extends; an immediate supertype.
- superclass of a class C (when used in this document without qualification): a class (not trait) which is one of the base classes of C.
- supertype of a class C (when used in this document without qualification): a class or trait which is one of the base classes of C.