9.7 Closures
Recall our first implementation of function greaterThan, in Listing 9.3:
Scala
def greaterThan(bound: Int): Int => Boolean = def greaterThanBound(x: Int): Boolean = x > bound greaterThanBound
You can apply greaterThan to different values to produce different functions. For example, greaterThan(5) is a function that tests if a number is greater than 5, while greaterThan(100) is a function that tests if a number is greater than 100:
Scala
val gt5 = greaterThan(5) val gt100 = greaterThan(100) gt5(90) // true gt100(90) // false
The question to ponder is this: In the gt5(90) computation, which compares 90 to 5, where does the value 5 come from? A 5 was pushed on the execution stack as local variable bound for the call greaterThan(5), but this call has been completed, and the value removed from the stack. In fact, another call has already taken place with local variable bound equal to 100. Still, gt5(90) compares to 5, not to 100. Somehow, the value 5 of variable bound was captured during the call greaterThan(5), and is now stored as part of function gt5.
The terminology surrounding this phenomenon is somewhat ambiguous, but most sources define closures to be functions associated with captured data.6 When a function, like greaterThanBound, uses variables in its body other than its arguments—here, bound—these variables must be captured to create a function value.
Closures are sometimes used in functional programming languages as a way to add state to a function. For instance, a function can be “memoized” (a form of caching) by storing the inputs and outputs of previous computations:
Scala
Listing 9.6: Memoization using closures; see also Lis. 12.2.
def memo[A, B](f: A => B): A => B = val store = mutable.Map.empty[A, B] def g(x: A): B = store.get(x) match case Some(y) => y case None => val y = f(x) store(x) = y y g
Function memo is a higher-order function. Its argument f is a function of type A => B. Its output is another function, g, of the same type. Function g is functionally equivalent to f—it computes the same thing—but stores every computed value into a map. When called on some input x, function g first looks up the map to see if value f(x) has already been calculated and if so, returns it. Otherwise, f(x) is computed, using function f, and stored in the map before being returned. You apply memo to a function to produce a memoized version of that function:
Scala
val memoLength: String => Int = memo(str => str.length) memoLength("foo") // invokes "foo".length and returns 3 memoLength("foo") // returns 3, without invoking method length
Function memoLength is a function from strings to integers, like str => str.length. It calculates the length of a string and stores it. The first time you call memoLength("foo"), the function invokes method length on string "foo", stores 3, and returns 3. If you call memoLength("foo") again, value 3 is returned directly, without invoking method length of strings. Another invocation memo(str => str.length) would create a new closure with its own store map.
What is captured by a closure is a lexical environment. This environment contains function arguments, local variables, and fields of an enclosing class, if any:
Scala
Listing 9.7: Example of a function writing in its closure.
def logging[A, B](name: String)(f: A => B): A => B = var count = 0 val logger = Logger.getLogger("my.package") def g(x: A): B = count += 1 logger.info(s"calling $name ($count) with $x") val y = f(x) logger.info(s"$name($x)=$y") y g
Like memo, function logging takes a function of type A => B as its argument and produces another function of the same type. The returned function is functionally equivalent to the input function, but it adds logging information, including the input and output of each call and the number of invocations:
Scala
val lenLog: String => Int = logging("length")(str => str.length) lenLog("foo") // INFO: calling length (1) with foo // INFO: length(foo)=3 lenLog("bar") // INFO: calling length (2) with bar // INFO: length(bar)=3
For this to work, the returned closure g needs to maintain references to arguments name and f, as well as to local variables count and logger.
Note that variable count is modified when the closure is called. Writing into closures can be a powerful mechanism, but it is also fraught with risks:
Scala
// DON'T DO THIS! val multipliers = Array.ofDim[Int => Int](10) var n = 0 while n < 10 do multipliers(n) = x => x * n n += 1
This code attempts to create an array of multiplying functions: It fills the array with functions of type Int => Int defined as x => x * n. The idea is that multipliers(i) should then be x => x * i, a function that multiplies its argument by i. However, as written, the implementation does not work:
Scala
val m3 = multipliers(3) m3(100) // 1000, not 300
All the functions stored in the array close over variable n and share it. Since n is equal to 10 at the end of the loop, all the functions in the array multiply their argument by 10 (at least, until n is modified). Some languages, including Java, emphasize safety over flexibility and do not allow local variables captured in closures to be written.
As with other forms of implicit references (e.g., inner classes), you need to be aware of closures to avoid tricky bugs caused by unintended sharing. This is especially true when closing over mutable data. As always, emphasizing immutability tends to result in safer code.