Reputation: 7162
While answering this question I stumbled upon a behavior I could not explain.
Coming from:
val builder = new StringBuilder("foo bar baz ")
(0 until 4) foreach { builder.append("!") }
builder.toString -> res1: String = foo bar baz !
The issue seemed clear, the function provided to the foreach was missing the Int argument, so StringBuilder.apply
got executed. But that does not really explain why it appends the '!' only once. So I got to experimenting..
I would have expected the following six statements to be equivalent, but the resulting Strings differ:
(0 until 4) foreach { builder.append("!") } -> res1: String = foo bar baz !
(0 until 4) foreach { builder.append("!")(_) } -> res1: String = foo bar baz !!!!
(0 until 4) foreach { i => builder.append("!")(i) } -> res1: String = foo bar baz !!!!
(0 until 4) foreach { builder.append("!").apply } -> res1: String = foo bar baz !
(0 until 4) foreach { builder.append("!").apply(_) } -> res1: String = foo bar baz !!!!
(0 until 4) foreach { i => builder.append("!").apply(i) } -> res1: String = foo bar baz !!!!
So the statements are obviously not equivalent. Can somebody explain the difference?
Upvotes: 4
Views: 242
Reputation: 55569
Let's label them:
A
- (0 until 4) foreach { builder.append("!").apply }
B
- (0 until 4) foreach { builder.append("!").apply(_) }
C
- (0 until 4) foreach { i => builder.append("!").apply(i) }
At first glance it is confusing, because it appears they should all be equivalent to each other. Let's look at C
first. If we look at it as a Function1
, it should be clear enough that builder.append("!")
is evaluated with each invocation.
val C = new Function1[Int, StringBuilder] {
def apply(i: Int): StringBuilder = builder.append("!").apply(i)
}
For each element in (0 to 4)
, C
is called, which re-evaluates builder.append("!")
on each invocation.
The important step to understanding this is that B
is syntactic sugar for C
, and not A
. Using the underscore in apply(_)
tells the compiler to create a new anonymous function i => builder.append("!").apply(i)
. We might not necessarily expect this because builder.append("!").apply
can be a function in it's own right, if eta-expanded. The compiler appears to prefer creating a new anonymous function, that simply wraps builder.append("!").apply
, rather than eta-expanding it.
From the SLS 6.23.1 - Placeholder Syntax for Anonymous Functions
An expression e of syntactic category Expr binds an underscore section u, if the following two conditions hold: (1) e properly contains u, and (2) there is no other expression of syntactic category Expr which is properly contained in e and which itself properly contains u.
So builder.append("!").apply(_)
properly contains the underscore, so the underscore syntax can applies for the anonymous function, and it becomes i => builder.append("!").apply(i)
, like C
.
Compare this to:
(0 until 4) foreach { builder.append("!").apply _ }
Here, the underscore is not properly contained in the expression, so the underscore syntax does not immediately apply as builder.append("!").apply _
can also mean eta-expansion. In this case, eta-expansion comes first, which will be equivalent to A
.
For A
, it is builder.append("!").apply
is implicitly eta-expanded to a function, which will only evaluate builder.append("!")
once. e.g. it is something like:
val A = new Function1[Int, Char] {
private val a = builder.append("!")
// append is not called on subsequent apply calls
def apply(i: Int): Char = a.apply(i)
}
Upvotes: 1
Reputation: 22085
scala.collection.mutable.StringBuilder
extends (Int => Char)
, and therefore builder.append("!")
, which returns a StringBuilder
, is a valid function argument to foreach
. The first line is therefore equivalent as if you wrote:
val f: Int => Char = builder.append("!").asInstanceOf[Int => Char] // appends "!" once
(0 until 4).foreach(f) // fetches the 0th to 3rd chars in the string builder, and does nothing with them
All the lines that append !!!! actually create a new anonymous function i => builder.append("!").apply(i)
, and are therefore equivalent to
val f: Int => Char = (i: Int) => builder.append("!").apply(i)
(0 until 4).foreach(f) // appends 4 times (and fetches the 0th to 3rd chars in the string builder, and does nothing with them)
As for your fourth line, it's weirder IMO. In that case, you are trying to read a "field" apply
in builder.append("!")
. But apply
is a method (Int)Char
, and the expected type (as determined by the param type of foreach
) is Int => ?
. So there is a way to lift the method apply(Int)Char
as an Int => ?
, which is to create a lambda that will call the method. But in this case, since you're trying to read apply
as field, initially, it means that the this
of .apply
should be evaluated once to be stored as a capture for the this
parameter of the method call, giving something equivalent to this:
val this$1: StringBuilder = builder.append("!") // appends "!" once
val f: Int => Char = (i: Int) => this$1.apply(i)
(0 until 4).foreach(f) // fetches the 0th to 3rd chars in the string builder, and does nothing with them
Upvotes: 1