What happens during template initialization?

2019-08-01 07:51发布

问题:

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.?

回答1:

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:

  1. 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.

  1. 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:

  1. a base class Base with some field
  2. a mix-in trait TASimple which contains 2 fields (one initial and one not initialized) and two methods
  3. 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):

  1. aValueNI and aValueI as a part of val declarations. Those must be implemented by SimpleChild backing them with some fields (no tricks whatsoever).

  2. aIncrementAndGetNI and aIncrementAndGetI methods with some logic. Those methods can be inherited by SimpleChild and will work basing on the aValueNI and aValueI methods.

  3. 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

  1. That all "base" fields are already initialized by the corresponding parent (simulated) constructors by the time the simulated constructor for some trait is called.

  2. 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.



标签: scala traits