Puzzler 3

Location, Location, Location

In many object-oriented languages, it is common to accept parameters in a class constructor for the purpose of assigning them to class members:

  class MyClass(param1, param2, ...) {
    val member1 = param1
    val member2 = param2
    ...
  }

Scala, which favors concise code, lets you avoid this redundancy by declaring members and constructor parameters in one go:

  class MyClass(val member1, val member2, ...) {
    ...
  }

What is the result of executing the following code?

  trait A {
    val audience: String
    println("Hello " + audience)
  }
  
class BMember(a: String = "World"extends A {   val audience = a   println("I repeat: Hello " + audience) }
class BConstructor(val audience: String = "World"extends A {   println("I repeat: Hello " + audience) }
new BMember("Readers") new BConstructor("Readers")

Possibilities

  1. Prints:
      Hello Readers
      I repeat: Hello Readers
      Hello Readers
      I repeat: Hello Readers
    
  2. Prints:
      Hello World
      I repeat: Hello Readers
      Hello World
      I repeat: Hello Readers
    
  3. Prints:
      Hello null
      I repeat: Hello Readers
      Hello Readers
      I repeat: Hello Readers
    
  4. Prints:
      Hello null
      I repeat: Hello Readers
      Hello null
      I repeat: Hello Readers
    

Explanation

The key question here is when precisely the assignment of "Readers" to audience becomes visible. You may also wonder whether or how the default value, "World", is involved. Surely the small optimization of moving the member declaration of audience into the constructor parameter list has no impact, though? Not so—the correct answer is number 3:

  scala> new BMember("Readers")
  Hello null
  I repeat: Hello Readers
  res3: BMember = BMember@1aa6f6eb
  
scala> new BConstructor("Readers") Hello Readers I repeat: Hello Readers res4: BConstructor = BConstructor@64b6603a

In other words, the value of audience in A differs if the member is declared in B's constructor parameters, as opposed to the constructor body.

To understand the difference between member declarations in the class body versus in the constructor parameter list, you need to examine Scala's class initialization sequence. Consider again the class declarations:

  class BMember(a: String = "World"extends A {
    ...
  }
  
class BConstructor(val audience: String = "World"extends A {   ... }

Both class declarations are of the form:[1]

  class c(param1) extends superclass { statements }

According to the language specification,[2] the initialization sequence for new BMember("Readers") and new BConstructor("Readers") will be:

  1. The argument "Readers" is evaluated. In this case, there is nothing to do here, but if the argument were specified as an expression (e.g., "readers".capitalize), this would be evaluated first.
  2. The class being constructed is initialized by evaluating the template:[3][4]

    superclass { statements }
    1. First, the superclass constructor A
    2. Then the statement sequence in the body of the subclass, either BMember or BConstructor

Here, note that we are omitting details regarding traits, etc., that do not apply to this example. In the case of BMember, "Readers" is assigned to the constructor parameter, a, in the first step. When A's constructor is invoked, audience is still uninitialized, so the default string value null is printed. "Readers" is assigned to audience, and then printed, only when the statement sequence in the body of BMember executes.

BConstructor's case is different: here "Readers" is evaluated and assigned to audience straight away, as part of the evaluation of the constructor arguments. The value of audience is already "Readers" by the time A's constructor is invoked.

Discussion

In general, the pattern in BConstructor is preferred as its behavior leaves less room for surprises. The val declared in the superclass never exists in an uninitialized state.

You can achieve the same result without declaring audience in the constructor parameter list by using an early field definition[5] clause. This allows you to perform additional computations on the constructor arguments (e.g., normalizing the case of string arguments), or to create anonymous classes with correctly initialized values:

  class BEarlyDef(a: String = "World"extends {
    val audience = a
  } with A {
    println("I repeat: Hello " + audience)
  }
  
scala> new BEarlyDef("Readers") Hello Readers I repeat: Hello Readers res7: BEarlyDef = BEarlyDef@44c93da7
scala> new {          val audience = "Readers"        } with A {          println("I repeat: Hello " + audience)        } Hello Readers I repeat: Hello Readers res0: A = anon1@71e16512

Early definitions define and assign member values before the supertype constructor is called. The initialization sequence, as per the sections of the language specification applicable in this case,[6] is:

A class is initialized by evaluating the template:

  1. First, early definitions in the order they are defined,
  2. then, the superclass constructor,
  3. finally, the statements in the default constructor.

In short, superclass and supertrait initialization code is executed after parameter evaluation and early field definitions and before the initialization statements of the class or trait being instantiated. The direct superclass and mixed-in traits are initialized in left-to-right order as they appear in the class, trait, or object definition.

An extended code sample puts all of this together:

  trait A {
    val audience: String
    println("Hello " + audience)
  }
  
trait AfterA {   val introduction: String   println(introduction) }
class BEvery(val audience: Stringextends {   val introduction =     { println("Evaluating early def"); "Are you there?" } } with A with AfterA {   println("I repeat: Hello " + audience) }
scala> new BEvery({ println("Evaluating param"); "Readers" }) Evaluating param Evaluating early def Hello Readers Are you there? I repeat: Hello Readers res3: BEvery = BEvery@6bcc2569
image images/moralgraphic117px.png Think of superclass constructors and supertrait initializers as being inserted, in left-to-right order of declaration, after the opening bracket of the class or object body (which forms the primary constructor).

Footnotes for Chapter 3:

[1] Odersky, The Scala Language Specification, Section 5.3. [Ode14]

[2] Odersky, The Scala Language Specification, Section 5.1. [Ode14]

[3] Odersky, The Scala Language Specification, Section 5.1. [Ode14]

[4] A template is the body of a class, trait, or singleton object definition. It defines the type signature, behavior, and initial state of a class, trait, or object.

[5] Odersky, The Scala Language Specification, Section 5.1.6. [Ode14]

[6] Odersky, The Scala Language Specification, Sections 5.1.1, 5.1, and 5.1.6. [Ode14]

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.202.61