In Scala official docs, it says that:
If this is the template of a trait then its mixin-evaluation consists
of an evaluation of the statement sequence statsstats.
If this is not a template of a trait, then its evaluation consists of
the following steps.
First, the superclass constructor sc is evaluated.
Then, all base classes in the template's linearization up to the template's
superclass denoted by sc are mixin-evaluated. Mixin-evaluation
happens in reverse order of occurrence in the linearization.
Finally, the statement sequence statsstats is evaluated.
I am wondering what the "mixin-evaluation" and "superclass constructor evaluation" mean here? Why is superclass constructor sc
treated differently from traits mt1
, mt2
, mt3
, etc.?
Well, this is one of those complicated things for which I don't think there is a good short answer unless you already know what the answer is. I think the short answer is that this is a result of the fact that Scala is compiled to the JVM bytecode and thus has to match restrictions of that target platform. Unfortunately I don't think this answer is clear so my real answer is going to be long.
Disclaimer: (a shameful self-promotion for the future readers): If you find this quite long answer useful, you might also take a look at my another long answer to another question by Lifu Huang on a similar topic.
Disclaimer: Java code in the translation examples is provided solely for illustrative purpose. They are inspired by what the Scala compiler actually does but don't match the "real thing" in many details. Moreover those examples are not guaranteed to work or compile. Unless explicitly mentioned otherwise I will use (simpler) code examples that are simplified versions of Scala 2.12/Java 8 translation rather than older (and more complicated) Scala/Java translations.
Some theory on mix-ins
Mixin is an idea in an object-oriented design to have some more or less encapsulated piece of logic that however doesn't make sense on its own and so it is added to some other classes. This is in a sense similar to multiple inheritance and multiple inheritance is actually the way this feature is designed in Scala.
If you want some real world examples of mix-ins in Scala, here are some:
- Scala collection library implementation is based on mix-ins. If you look at definition of something like
scala.collection.immutable.List
you'll see a lot of mix-ins
sealed abstract class List[+A] extends AbstractSeq[A]
with LinearSeq[A]
with Product // this one is not a mix-in!
with GenericTraversableTemplate[A, List]
with LinearSeqOptimized[A, List[A]] {
In this example mix-ins are used to share implementation of advanced methods via core methods along the deep and wide Scala collections hierarchy.
- Cake pattern used for dependency injection is based on mix-ins but this time the mixed-in logic typically doesn't make sense on its own at all.
What is important here is that in Scala you can mix-in both logic (methods) and data (fields).
Some theory on Java/JVM and multiple inheritance
"Naive" multiple inheritance as done in languages like C++ has an infamous Diamond problem. To fix it original design of Java didn't support multiple-inheritance of any logic or fields. You may "extend" exactly one base class (fully inheriting its behavior) and additionally you can "implement" many interfaces which means that the class claims to have all methods from the interface(s) but you can't have any real logic inherited from your base interface. The same restrictions existed in the JVM. 20 years later in Java 8 Default Methods were added. So now you can inherit some methods but still can't inherit any fields. This simplified implementation of mix-ins in Scala 2.12 at the price of requiring Java 8 as its target platform. Still interfaces can't have (non-static) fields and thus can't have constructors. This is one of the major reasons why the superclass constructor sc
is treated differently from the traits mt1
, mt2
, mt3
, etc.
Also it is important to note that Java was designed as a pretty safe language. Particularly it fights against "undefined behaviors" that might happen if you re-use some values that are just leftover (garbage) in the memory. So Java ensures that you can't access any fields of the base class until its constructor is called. This makes super
call pretty much mandatory first line in any child constructor.
Scala and mix-ins (simple example)
So now imagine you are a designer of the Scala language and you want it to have mix-ins but your target platform (JVM) doesn't support them. What should you do? Obviously your compiler should be able to convert mix-ins into something that JVM supports. And here is a rough approximation of how it is done for a simple (and nonsense) example:
class Base(val baseValue: Int) {
}
trait TASimple {
val aValueNI: AtomicInteger
val aValueI: AtomicInteger = new AtomicInteger(0)
def aIncrementAndGetNI(): Int = aValueNI.incrementAndGet()
def aIncrementAndGetI(): Int = aValueI.incrementAndGet()
}
class SimpleChild(val childValue: Int, baseValue: Int) extends Base(baseValue) with TASimple {
override val aValueNI = new AtomicInteger(5)
}
So in words you have:
- a base class
Base
with some field
- a mix-in trait
TASimple
which contains 2 fields (one initial and one not initialized) and two methods
- a child class
SimpleChild
Since TASimple
is more than just a declaration of methods, it can't be compiled to just a simple Java interface. It is actually compiled into something like this (in Java code):
public abstract interface TASimple
{
abstract void TASimple_setter_aValueI(AtomicInteger param);
abstract AtomicInteger aValueNI();
abstract AtomicInteger aValueI();
default int aIncrementAndGetNI() { return aValueNI().incrementAndGet(); }
default int aIncrementAndGetI() { return aValueI().incrementAndGet(); }
public static void init(TASimple $this)
{
$this.TASimple_setter_aValueI(new AtomicInteger(0));
}
}
public class SimpleChild extends Base implements TASimple
{
private final int childValue;
private final AtomicInteger aValueNI;
private final AtomicInteger aValueI;
public AtomicInteger aValueI() { return this.aValueI; }
public void TASimple_setter_aValueI(AtomicInteger param) { this.aValueI = param; }
public int childValue() { return this.childValue; }
public AtomicInteger aValueNI() { return this.aValueNI; }
public SimpleChild(int childValue, int baseValue)
{
super(baseValue);
TASimple.init(this);
this.aValueNI = new AtomicInteger(5);
}
}
So what TASimple
contains and how it is translated (to Java 8):
aValueNI
and aValueI
as a part of val
declarations. Those must be implemented by SimpleChild
backing them with some fields (no tricks whatsoever).
aIncrementAndGetNI
and aIncrementAndGetI
methods with some logic. Those methods can be inherited by SimpleChild
and will work basing on the aValueNI
and aValueI
methods.
A piece of logic that initializes aValueI
. If TASimple
was a class, it would have a constructor and this logic might have been there. However TASimple
is translated to an interface. Thus that "constructor" piece of logic is moved to a static void init(TASimple $this)
method and that init
is called from the SimpleChild
constructor. Note that Java spec enforces that the super
call (i.e. the constructor of the base class) must be called before it.
The logic in the item #3 is what stands behind
First, the superclass constructor sc is evaluated.
Then, all base classes in the template's linearization up to the template's superclass denoted by sc are mixin-evaluated
Again this is the logic enforce by the JVM itself: you first have to call the base constructor and only then you can (and should) call all the other simulated "constructors" of all mix-ins.
Side note (Scala pre-2.12/Java pre-8)
Before Java 8 and default methods translation would be even more complicated. TASimple
would be translated into an interface and class such as
public abstract interface TASimple
{
public abstract void TASimple_setter_aValueI(AtomicInteger param);
public abstract AtomicInteger aValueNI();
public abstract AtomicInteger aValueI();
public abstract int aIncrementAndGetNI();
public abstract int aIncrementAndGetI();
}
public abstract class TASimpleImpl
{
public static int aIncrementAndGetNI(TASimple $this) { return $this.aValueNI().incrementAndGet(); }
public static int aIncrementAndGetI(TASimple $this) { return $this.aValueI().incrementAndGet(); }
public static void init(TASimple $this)
{
$this.TASimple_setter_aValueI(new AtomicInteger(0));
}
}
public class SimpleChild extends Base implements TASimple
{
private final int childValue;
private final AtomicInteger aValueNI;
private final AtomicInteger aValueI;
public AtomicInteger aValueI() { return this.aValueI; }
public void TASimple_setter_aValueI(AtomicInteger param) { this.aValueI = param; }
public int aIncrementAndGetNI() { return TASimpleImpl.aIncrementAndGetNI(this); }
public int aIncrementAndGetI() { return TASimpleImpl.aIncrementAndGetI(this); }
public int childValue() { return this.childValue; }
public AtomicInteger aValueNI() { return this.aValueNI; }
public SimpleChild(int childValue, int baseValue)
{
super(baseValue);
TASimpleImpl.init(this);
this.aValueNI = new AtomicInteger(5);
}
}
Note how now implementations of aIncrementAndGetNI
and aIncrementAndGetI
are moved to some static methods that take explicit $this
as a parameter.
Scala and mix-ins #2 (complicated example)
Example in the previous section illustrated some of the ideas but not all of them. For a more detailed illustration a more complicated example is required.
Mixin-evaluation happens in reverse order of occurrence in the linearization.
This part is relevant when you have several mix-ins and especially in the case of the diamond problem. Consider following example:
trait TA {
val aValueNI0: AtomicInteger
val aValueNI1: AtomicInteger
val aValueNI2: AtomicInteger
val aValueNI12: AtomicInteger
val aValueI: AtomicInteger = new AtomicInteger(0)
def aIncrementAndGetNI0(): Int = aValueNI0.incrementAndGet()
def aIncrementAndGetNI1(): Int = aValueNI1.incrementAndGet()
def aIncrementAndGetNI2(): Int = aValueNI2.incrementAndGet()
def aIncrementAndGetNI12(): Int = aValueNI12.incrementAndGet()
def aIncrementAndGetI(): Int = aValueI.incrementAndGet()
}
trait TB1 extends TA {
val b1ValueNI: AtomicInteger
val b1ValueI: AtomicInteger = new AtomicInteger(1)
override val aValueNI1: AtomicInteger = new AtomicInteger(11)
override val aValueNI12: AtomicInteger = new AtomicInteger(111)
def b1IncrementAndGetNI(): Int = b1ValueNI.incrementAndGet()
def b1IncrementAndGetI(): Int = b1ValueI.incrementAndGet()
}
trait TB2 extends TA {
val b2ValueNI: AtomicInteger
val b2ValueI: AtomicInteger = new AtomicInteger(2)
override val aValueNI2: AtomicInteger = new AtomicInteger(22)
override val aValueNI12: AtomicInteger = new AtomicInteger(222)
def b2IncrementAndGetNI(): Int = b2ValueNI.incrementAndGet()
def b2IncrementAndGetI(): Int = b2ValueI.incrementAndGet()
}
class Base(val baseValue: Int) {
}
class ComplicatedChild(val childValue: Int, baseValue: Int) extends Base(baseValue) with TB1 with TB2 {
override val aValueNI0 = new AtomicInteger(5)
override val b1ValueNI = new AtomicInteger(6)
override val b2ValueNI = new AtomicInteger(7)
}
What is interesting here is that ComplicatedChild
inherits from TA
in two ways: via TB1
and TB2
. Moreover both TB1
and TB2
define some initialization of aValueNI12
but with different values. First of all it should be mentioned that ComplicatedChild
will have only one copy of fields for each val
defined in TA
. But then what would happen if you try this:
val cc = new inheritance.ComplicatedChild(42, 12345)
println(cc.aIncrementAndGetNI12())
Which value (TB1
or TB2
) would win? And will the behavior be deterministic at all? The answer to the last question is - yes, the behavior will be deterministic both between runs and between compilations. This is achieved via so called "traits linearization" which is an entirely different topic. In short the Scala compiler sorts all the inherited (directly and indirectly) traits in some fixed defined order such that it manifests some good behaviors (such as the parent trait is always after its child trait in the list). So going back to the quote:
Mixin-evaluation happens in reverse order of occurrence in the linearization.
This traits linearization order ensures
That all "base" fields are already initialized by the corresponding parent (simulated) constructors by the time the simulated constructor for some trait is called.
Order of the simulated constructors calls is fixed so behavior is deterministic.
In this particular case the linearization order will be ComplicatedChild
> TB2
> TB1
> TA
> Base
. It means that ComplicatedChild
constructor is actually translated into something like:
public ComplicatedChild(int childValue, int baseValue)
{
super(baseValue);
TA.init(this);
TB1.init(this);
TB2.init(this);
this.aValueNI0 = new AtomicInteger(5);
this.b1ValueNI = new AtomicInteger(6);
this.b2ValueNI = new AtomicInteger(7);
}
and so aValueNI12
will be initialized by TB2
(which will overwrite the value set by the TB1
"constructor").
Hope this clarifies a bit what's going on and why. Let me know if something is not clear.
Update (answer to comment)
The spec says
Then, all base classes in the template's linearization up to the template's superclass denoted by scsc are mixin-evaluated. Mixin-evaluation happens in reverse order of occurrence in the linearization.
what does the “up to” precisely mean here?
Let's extend the "simple" example adding one more base trait
as following:
trait TX0 {
val xValueI: AtomicInteger = new AtomicInteger(-1)
}
class Base(val baseValue: Int) extends TX0 {
}
trait TASimple extends TX0 {
val aValueNI: AtomicInteger
val aValueI: AtomicInteger = new AtomicInteger(0)
def aIncrementAndGetNI(): Int = aValueNI.incrementAndGet()
def aIncrementAndGetI(): Int = aValueI.incrementAndGet()
}
class SimpleChild(val childValue: Int, baseValue: Int) extends Base(baseValue) with TASimple {
override val aValueNI = new AtomicInteger(5)
}
Note how here TX0
is inherited by both BaseClass
and TASimple
. In this case I expect linearization to produce the following order SimpleChild
> TASimple
> Base
> TX0
> Any
. I interpret that sentence as following: in this case the constructor of the SimpleChild
will not call the "simulated" constructor of the TX0
because it comes in the order after the Base
(= sc
). I think the logic for this behavior is obvious: from the point of view of the SimpleChild
constructor the "simulated" constructor of the TX0
should have already been called by the Base
constructor, moreover Base
might have updated results of that call so calling the "simulated" constructor of the TX0
second time might actually break the Base
.