Puzzler 19

What's in a Name?

Relying on the precise order of items in a parameter list is fragile, especially if parameters are of the same type:

  def inEcosystem(predator: String, prey: String) { 
    println(s"${predator} eat ${prey}")
  }
  
scala> inEcosystem("cats""mice") cats eat mice
def inEcosystem(prey: String, predator: String) {    println(s"${prey} are eaten by ${predator}") }
// no idea that the definition has changed... scala> inEcosystem("cats""mice") cats are eaten by mice
scala> inEcosystem(predator = "cats", prey = "mice") mice are eaten by cats

Scala also supports default arguments,[1] making it easy to write versatile functions and provide for common use cases without a combinatorial explosion of overloaded methods:

  object MakeSequences {
    def mkSeq(end: Int, start: Int = 1, step: Int = 1) = ...
  
  // rather than...   def mkSeq2(end: Int, start: Int, step: Int) = ...   def mkSeq2(end: Int, start: Int) = mkSeq2(end, start, 1)   def mkSeq2(end: Int) = mkSeq2(end, 11) }

This puzzler uses a combination of named and default arguments to invoke an overridden method. What is the result of executing the following code in the REPL?

  class SimpleAdder {
    def add(x: Int = 1, y: Int = 2): Int = x + y
  }
  class AdderWithBonus extends SimpleAdder {
    override def add(y: Int = 3, x: Int = 4): Int = 
      super.add(x, y) + 10
  }
  
val adder: SimpleAdder = new AdderWithBonus adder add (y = 0) adder add 0

Possibilities

  1. Prints:
      14
      12
    
  2. Prints:
      14
      14
    
  3. Prints:
      13
      14
    
  4. Prints:
      11
      12
    

Explanation

You may wonder whether declaring adder as a SimpleAdder somehow causes the default values of its add method to be applied, printing 11 and 12. Alternatively, if the overriding version in AdderWithBonus is chosen, surely 14 and 14 would then be displayed?

Not so. The correct answer is number 3:

  scala> adder add (y = 0)
  res10: Int = 13
  
scala> adder add 0 res11: Int = 14

Given the possible values, the only way this outcome can be achieved is if the default value for x in class AdderWithBonus is used in both cases, i.e., even when y = 0 is specified.

How can this be? To understand the result, you need to examine how Scala handles named and default arguments.[2] Since the JVM does not support these natively, the compiler needs to transform an invocation with such arguments into a "regular" call in which all arguments are passed in the order required by the function definition.

The compiler accomplishes this by defining a variable for each parameter required by the function and assigning the value of all provided arguments, positional and named, to the appropriate variables. For all other parameters, the compiler invokes special "default methods" that are automatically added to classes that define default parameter values.[3] The target method is then invoked with all arguments. In the second statement, adder add 0, this turns into the following:

  { 
    val x$1 = 0 // arg at position 0
    val x$2 = adder.add$default$2
    adder.add(x$1, x$2)
  }
  res12: Int = 14

In the case of adder add (y = 0), an additional step is required. The compiler needs to rearrange the variables so that the correct values appear at the expected positions. It does this using the only information available when the call is compiled: the method definition of the type of the value being invoked.

Since adder's type is SimpleAdder—not AdderWithBonus, which is its runtime type—this means that the compiler moves the value 0 to the position of parameter y on SimpleAdder's definition of add:

  { 
    val x$1 = 0 // named arg for 'y'
    val x$2 = adder.add$default$1
    adder.add(x$2, x$1) // matches order in SimpleAdder.add
  }
  res13: Int = 13

The values of the default arguments are determined at runtime by invoking default methods—here, add$default$1. The new defaults specified in AdderWithBonus cause the compiler to generate overrides for the default methods in SimpleAdder. As a result, the default parameter values defined for AdderWithBonus, the runtime type of adder, are used.

Discussion

If the type of adder is not explicitly specified, the result is less surprising:

  val adder2 = new AdderWithBonus
  
scala> adder2 add (y = 0) res14: Int = 14
scala> adder2 add 0 res15: Int = 14

Obviously, the easiest way to avoid the scenario in the main code sample is to ensure that the order of parameters in AdderWithBonus.add matches those of the overridden version:

  class SimpleAdder {
    def add(x: Int = 1, y: Int = 2): Int = x + y
  }
  class AdderWithBonus2 extends SimpleAdder {
    override def add(x: Int = 4, y: Int = 3): Int = 
      super.add(x, y) + 10
  }
  
val adder3: SimpleAdder = new AdderWithBonus2 scala> adder3 add (y = 0) res16: Int = 14
scala> adder3 add 0 res17: Int = 13
val adder4 = new AdderWithBonus2 scala> adder4 add (y = 0) res18: Int = 14
scala> adder4 add 0 res19: Int = 13
image images/moralgraphic117px.png
  1. Where possible, preserve the parameter order when overriding methods.
  2. When using named and default arguments, remember (to paraphrase Josh Suereth): "Names are compile time; values are runtime."

Footnotes for Chapter 19:

[1] See Puzzler 16 for a more detailed discussion of default arguments.

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

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

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

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