Puzzler 17

Implicitly Surprising

The ability to create and pass around partially applied functions helps you to move from specific to more general and reusable functionality. Instead of a method that increments a value by two, for instance, we can write a more generally applicable add method and specialize it for our use case:

  def add(x: Int)(y: Int) = x + y
  def addTo2 = add(2) _
  
scala> addTo2(3) res2: Int = 5

Scala also supports implicits, which, among other things, allows you to pick up arguments from the context rather than passing them explicitly to methods and functions.

How do these two play together? What is the result of executing the following code in the REPL?

  implicit val z1 = 2
  def add(x: Int)(y: Int)(implicit z: Int) = x + y + z
  def addTo(n: Int) = add(n) _
  
implicit val z2 = 3 val addTo1 = addTo(1) addTo1(2) addTo1(2)(3)

Possibilities

  1. Prints:
      5
      6
    
  2. Prints:
      6
      6
    
  3. The first invocation of addTo1 prints:
      5
    

    and the second fails to compile.

  4. The first invocation of addTo1 fails to compile, and the second prints:
      6
    

Explanation

You may wonder whether add's implicit parameter becomes an implicit parameter of the partially applied function that is returned by addTo, or whether it is somehow "converted" to a regular parameter list. If addTo1 has an implicit parameter, or if the compiler instead attempts to resolve the implicit inside the invocation of addTo, will you run into problems with ambiguous implicit values for z?

Neither, in fact. The correct answer is number 3:

  scala> addTo1(2)
  res0: Int = 5
  
scala> addTo1(2)(3) <console>:12: error: Int does not take parameters               addTo1(2)(3)                        ^

What happened to method add's implicit parameter? And how was a clash between the two ambiguous implicit values z1 and z2 avoided?

To address the first question, first observe that the type signature of addTo1 shows that it is a function of one, not two parameters:

  scala> val addTo1 = addTo(1)
  addTo1: Int => Int = <function1>

In other words, while add's regular parameter y is also a parameter of the eta-expanded function addTo1, the implicit parameter z is not. Instead, the implicit is resolved at the moment eta expansion[1] is applied in the body of method addTo.

How does the compiler choose the implicit value z1, however? After all, by the time addTo is invoked in the expression val addTo1 = addTo(1), both z1 and z2 are in scope.

The explanation is straightforward: the implicit is resolved by the compiler when method addTo is compiled, not when it is later invoked.[2] At that point, the only implicit in scope is z1. The method addTo effectively becomes:

  def add(x: Int)(y: Int) = x + y + 2
  def addToWithResolvedImplicit(n: Int) = add(n) _

This behaves in the same way as the original addTo method:

  scala> val addTo1WithResolvedImplicit = 
           addToWithResolvedImplicit(1)
  addTo1WithResolvedImplicit: Int => Int = <function1>
  
scala> addTo1WithResolvedImplicit(2) res2: Int = 5
scala> addTo1WithResolvedImplicit(2)(3) <console>:12: error: Int does not take parameters               addTo1WithResolvedImplicit(2)(3)                                            ^

Discussion

If the implicit values z1 and z2 were both in scope when addTo is declared, the code would indeed fail to compile:

  implicit val z1 = 2
  implicit val z2 = 3
  
def add(x: Int)(y: Int)(implicit z: Int) = x + y + z
scala> def addTo(n: Int) = add(n) _ <console>:10: error: ambiguous implicit values:  both value z1 of type => Int  and value z2 of type => Int  match expected type Int        def addTo(n: Int) = add(n) _                               ^

Resolution of the implicit parameter cannot be prevented using eta expansion, but you can retain z as a parameter by avoiding eta expansion entirely—just explicitly construct an anonymous function to return:

  def addToReturnAnonFun(n: Int) =
    (y: Int) => (z: Int) => add(n)(y)(z)
  
scala> val addTo1ReturnAnonFun = addToReturnAnonFun(1) addTo1ReturnAnonFun: Int => (Int => Int) = <function1>
scala> addTo1ReturnAnonFun(2) res0: Int => Int = <function1>
scala> addTo1ReturnAnonFun(2)(3) res1: Int = 6

The addToReturnAnonFun method creates and returns a curried function with two parameter lists, consistent with the two parameter lists (y) and (z) of the add method. If you try to use placeholder syntax to make the definition of the anonymous function more concise, you end up with a function with two parameters, not a curried function:

Anonymous Functions and Implicit Parameters

You might reasonably assume that implicit parameters are resolved during eta expansion because anonymous functions simply do not support implicit parameters. In fact they do.[3]

The semantics of implicit parameters in anonymous functions is slightly different, however, from that of implicit parameters in methods: the compiler will not automatically resolve them—they need to be explicitly specified when the anonymous function is invoked.

How do implicit parameters in anonymous functions differ from regular ones, then? The purpose of the implicit keyword is to make the value eligible for implicit resolution in the body of the anonymous function. A short example can demonstrate this:

  def iNeedAnImplicit(implicit n: Int) = n + 1
  
scala> val anonFun = { x: Int => y: Int =>           x + y + iNeedAnImplicit } <console>:8: error: could not find implicit value for    parameter n: Int        val anonFun = { x: Int => y: Int =>          x + y + iNeedAnImplicit }                  ^
scala> val anonFunWithImplicitParam = { x: Int =>           implicit y: Int => x + y + iNeedAnImplicit } anonFunWithImplicitParam:    Int => (Int => Int) = <function1>
implicit val z = 2
scala> anonFunWithImplicitParam(1)(2) res4: Int = 6
// the compiler will not supply the implicit argument scala> anonFunWithImplicitParam(1) res5: Int => Int = <function1>

  def addToReturnPlaceholderAnonFun(n: Int) =
    add(n)(_: Int)(_: Int)
  
scala> val addTo1ReturnPlaceholderAnonFun =          addToReturnPlaceholderAnonFun(1) addTo1ReturnPlaceholderAnonFun: (Int, Int) => Int = <function2>

In both cases, the second parameter of the partially applied function is not implicit, and must be explicitly provided when invoking the function.

image images/moralgraphic117px.png Bear in mind that, during eta expansion, implicit parameters are resolved. They are not parameters of the resulting function value.

Footnotes for Chapter 17:

[1] Eta expansion is discussed in more detail in Puzzler 15.

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

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

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

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