Puzzler 26

Accepts Any Args

Scala supports both multiple-parameter and multiple-parameter list (i.e., curried) definitions of functions. This gives you quite a bit of freedom when defining functions, but also means that some rework can be required if you decide to change a function's "parameter style."

In the following example, a method that starts out with a single parameter list with two parameters is refactored to use curried parameters. Both forms of the method are then invoked, unchanged, before and after refactoring.

What is the result of executing the following code?

  def prependIfLong(candidate: Any, elems: Any*): Seq[Any] = {
    if (candidate.toString.length > 1)
      candidate +: elems
    else
      elems
  }
  println(prependIfLong("I""love""Scala")(0))
  
def prependIfLongRefac(candidate: Any)(elems: Any*):     Seq[Any] = {   if (candidate.toString.length > 1)     candidate +: elems   else     elems } // invoked unchanged println(prependIfLongRefac("I""love""Scala")(0)) 

Possibilities

  1. Prints:
      love
      love
    
  2. Prints:
      love
      ArrayBuffer((I,love,Scala), 0)
    
  3. Prints:
      love
      IloveScala
    
  4. The first println statement prints:
      love
    

    and the second fails to compile.

Explanation

You may wonder whether the compiler can apply some kind of "compatibility transformation" to adapt the unchanged invocation to the new curried declaration. If not, the second prependIfLongRefac invocation must surely fail to compile?

Not quite:

  scala> println(prependIfLong("I""love""Scala")(0))
  love
  
scala> println(prependIfLongRefac("I""love""Scala")(0)) ArrayBuffer((I,love,Scala), 0)

The correct answer, therefore, is number 2. Let's first briefly step through the "pre-refactoring" invocation, which behaves as expected. Here, the first argument, "I", is bound to prependIfLong's candidate parameter, and "love" and "Scala" are bound to the second vararg parameter, elems. "I" is not longer than one character, so prependIfLong returns the vararg sequence WrappedArray("love", "Scala") unchanged. Its first element "love" is then printed:

  val prepended = prependIfLong("I""love""Scala")
  
scala> println(prepended(0)) love

So far, so straightforward. The second invocation, after the refactoring, is where things get interesting. The most surprising fact is perhaps that the unchanged invocation still compiles successfully. The println statement produces a rather unexpected output, though: you certainly no longer appear to be printing the first element of the returned sequence.

How does the compiler manage to compile this call? After refactoring, prependIfLongRefac expects an initial argument list with a single argument. Yet three arguments are still being passed, matching the original multi-parameter declaration of prependIfLong!

Here, a feature not documented in the language specification comes into play. After the compiler has unsuccessfully attempted to find a version of prependIfLongRefac that can take three arguments, it tries one last option: packing all arguments into a tuple and trying to apply the function to that. This auto-tupling actually succeeds in this case, binding candidate to the three-tuple, ("I", "love", "Scala").

At this point, all the arguments intended for prependIfLongRefac have been exhausted, so what about the second vararg parameter, elems? Here, the compiler simply "grabs" the (0) application that is supposed to extract the first element from the method result! It therefore ends up printing the entire result consisting (since candidate.toString is certainly longer than one character) of candidate prepended to elems.

In other words, the second invocation is equivalent to:

  val prepended = prependIfLongRefac(("I""love""Scala"))(0)
  
scala> println(prepended) ArrayBuffer((I,love,Scala), 0)

Discussion

You can use the -Ywarn-adapted-args and -Yno-adapted-args compiler flags to warn and fail, respectively, on occurrences of auto-tupling:[1]

  • -Ywarn-adapted-args Warn if an argument list is modified to match the receiver.
  scala> :settings +Ywarn-adapted-args
  
scala> println(prependIfLongRefac("I""love""Scala") _) <console>:9: warning: Adapting argument list by creating   a 3-tuple: this may not be what you want.         signature: prependIfLongRefac(candidate: Any)           (elems: Any*): Seq[Any]   given arguments: "I", "love", "Scala"  after adaptation: prependIfLongRefac(("I", "love", "Scala"):     (String, String, String))               println(prependIfLongRefac(                                         ^ <function1>
  • -Yno-adapted-args Do not adapt an argument list to match the receiver.
  scala> :settings +Yno-adapted-args
  
scala> println(prependIfLongRefac("I""love""Scala") _) <console>:9: error: too many arguments for method   prependIfLongRefac: (candidate: Any)(elems: Any*)Seq[Any]               println(prependIfLongRefac(                                         ^

Specifying the first argument as a named argument is another way to prevent the compiler from silently auto-tupling:

  scala> println(prependIfLong(
           candidate = "I""love""Scala")(0))
  love
  
scala> println(prependIfLongRefac(          candidate = "I""love""Scala")(0)) <console>:9: error: too many arguments for method    prependIfLongRefac: (candidate: Any)(elems: Any*)Seq[Any]               println(prependIfLongRefac(                                         ^
image images/moralgraphic117px.png When defining methods or functions with parameters of type Any, and especially when refactoring such methods, be aware of the possibility of auto-tupling. If desired, use the compiler flags -Ywarn-adapted-args and -Yno-adapted-args to warn of or prevent auto-tupling.

Footnotes for Chapter 26:

[1] Unfortunately, the warnings and failures are not triggered if the adaptation does not occur in the last argument list.

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

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