Sascha Kolberg
Sascha Kolberg

Reputation: 7162

Unexpected behavior of StringBuilder in foreach

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

Answers (2)

Michael Zajac
Michael Zajac

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

sjrd
sjrd

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

Related Questions