Puzzler 16

One Bound, Two to Go

Through currying, Scala allows methods to be defined with multiple parameter lists. Consider the following two method definitions:

  def regular(x: Int, y: Int, z: Int) = x + y + z
  def curried(x: Int)(y: Int)(z: Int) = x + y + z

Both methods compute the same result when given identical values for their corresponding arguments:

  scala> regular(123)
  res0: Int = 6
  
scala> curried(1)(2)(3) res1: Int = 6

Despite this similarity, the curried method is fundamentally different from the regular one: the invocation of curried represents three consecutive function invocations.[1]

Scala also supports default arguments, which significantly reduces the need for method overloading. Furthermore, by combining default arguments with named arguments, a subset of arguments can be provided when invoking a method, in any order. This allows you to invoke the method more flexibly and makes the call site more robust against refactoring errors.

The following program defines a curried method with two parameter lists. Default argument values are stipulated for the parameters in the second list. What is the result of executing the following code in the Scala REPL?

  def invert(v3: Int)(v2: Int = 2, v1: Int = 1) {
    println(v1 + ", " + v2 + ", " + v3)
  }
  
val invert3 = invert(3) _
invert3(v1 = 2) invert3(v1 = 2, v2 = 1)

Possibilities

  1. Prints:
      2, 2, 3
      2, 1, 3
    
  2. The first invocation of invert3 fails to compile, and the second one prints:
      1, 2, 3
    
  3. The first invocation of invert3 fails to compile, and the second one prints:
      2, 1, 3
    
  4. The first invocation of invert3 fails to compile, and the second one prints:
      2, 2, 3
    

Explanation

The type of invert3 is a function type, because only the first parameter list is provided when invert is invoked:

  scala> val invert3 = invert(3) _
  invert3: (Int, Int) => Unit = <function2>

Function invert3 takes two arguments of type Int, but the first invocation supplies only one: v1 = 2. Parameter v2 has a default value of 2, so you might assume that the first invocation of invert3 would print:

  2, 2, 3

And you would then expect the second invocation to print:

  2, 1, 3

Let's verify:

  scala> invert3(v1 = 2)
  <console>:10: error: not enough arguments for method apply: 
    (v1: Int, v2: Int)Unit in trait Function2.
  Unspecified value parameter v2.
                invert3(v1 = 2)
                       ^
  
scala> invert3(v1 = 2, v2 = 1) 1, 2, 3

So, not the output of the first candidate answer. It turns out the correct answer is number 2! What is going on?

The compiler error gives us a hint: the answer lies in understanding how function types are actually implemented. As stated in The Scala Language Specification,[2] they represent shorthands for anonymous class types that have an apply method, defined as follows:[3]

  // abstract apply method of Function2[T1, T2, R]
  def apply(v1: T1, v2: T2): R

Therefore, the original program is expanded during compilation to something along these lines:

  def invert(v3: Int)(v2: Int = 2, v1: Int = 1) {
    println(v1 + ", " + v2 + ", " + v3)
  }
  
def invert3 = new Function2[Int, Int, Unit] {   def apply(v1: Int, v2: Int): Unit = invert(3)(v1, v2) }
invert3.apply(v1 = 2) invert3.apply(v1 = 2, v2 = 1)

Two things stand out about the signature of method apply. First, there are no default values for the method parameters. As a consequence, the first invocation of invert3 results in a compiler error, since only one argument, v1 = 2, is being passed.

Second, the parameters are actually called v1 and v2. Had the invert method used different names for its parameters, even the second invocation of invert3 would have failed to compile. The parameters of the apply method on all Function2 instances have such names; there is no link to the parameter names of the initial invert method.

Finally, because invert3 happens to have the same parameter names, but in the reverse order, the second invocation prints:

  scala> invert3(v1 = 2, v2 = 1)
  1, 2, 3

Even though you might think that the named arguments pertain to invert, they actually refer to the parameters of the apply method. More precisely, parameter v1 of apply, which corresponds to v2 of invert, is set to 2. Likewise, parameter v2 of method apply, corresponding to v1 of invert, is set to 1.

Discussion

You may wonder about the rationale for defining parameters of the invert method in separate parameter lists. There are several reasons you might decide to define method parameters in such a manner, rather than using a single parameter list. First, you can refer to the parameters of a previous parameter list when defining default arguments:

  def area(x: Int)(y: Int = x) = x * y

Multiple parameter lists can also facilitate type inference, because types inferred in earlier parameter lists need not be specified. Take the foldLeft method from Scala's collection library as an example:[4]

  def foldLeft[B](z: B)(op: (B, A) => B): B
  // no need to specify Int for B
  Seq("I""II""III").foldLeft(0)(_ + _.length) 

Multiple parameter lists allow you to have both implicit and non-implicit parameters:

  def maxBy[B](f: A => B)(implicit cmp: Ordering[B]): A

And they let you define fluent APIs that use curly braces instead of parentheses to surround a parameter list:[5]

  def benchmark(times: Int)(block: => Unit): Unit
  benchmark(10000) {
    ...
  }

One final benefit of currying illustrated by this puzzler is that multiple parameter lists can allow partially applied functions to be expressed concisely. For example, imagine that method invert were declared using a single parameter list:

  def invert(v3: Int, v2: Int = 2, v1: Int = 1) {
    println(v1 + ", " + v2 + ", " + v3)
  }

You could not apply this form of invert to only one argument using the following syntax:

  scala> def invert3 = invert(3) _
  <console>:8: error: _ must follow method; cannot follow Unit
         def invert3 = invert(3) _
                             ^

You would need to provide underscores and a type ascription for each missing argument:

  scala> def invert3 = invert(3, _: Int, _: Int)
  invert3: (Int, Int) => Unit

When it comes to methods with single parameter lists, the only case where you can get away with not specifying argument types is when you pass an underscore for all arguments:

  scala> def invert3 = invert(_, _, _)
  invert3: (Int, Int, Int) => Unit
image images/moralgraphic117px.png Bear in mind that when you are invoking a partially applied function, named arguments do not resolve against the original method, but against a generated function object. You can avoid problems by steering clear of parameter names used by Scala's function traits.

Footnotes for Chapter 16:

[1] The compiler can turn these consecutive invocations into a single method call if all arguments are provided.

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

[3] See the Scaladoc for Function2. [EPF]

[4] See Puzzler 6 for an additional discussion of type parameters that occur in multiple parameter lists.

[5] For a more detailed discussion of curly braces versus parentheses, see Puzzler 35.

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

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