Scope, Lifetime, and Closures
This walkthrough uses three small Scala examples to show how closures capture variables from their defining environment. We move from simple partial application to stateful closures, then to a practical higher-order function pattern (memoization).
1) Closure from a local function (partial application style)
A function returns another function that remembers the greeting prefix s.
def greeter(s: String) : String=>String =
def g(t: String) : String =
s"$s, $t!"
end g
g
end greeter
val greet = greeter("Hello")
println(greet("Alice")) // Hello, Alice!
println(greet("Bob")) // Hello, Bob!The returned function g outlives its definition site, but still has access to s. Next, we use the same mechanism to preserve evolving state across calls.
2) Stateful closure: cumulative average
The function cumulativeAvg creates local variables that are captured by the returned function. Each call updates shared state and computes the running average.
def cumulativeAvg() : Int=>Int =
var sum = 0
var count = -1
def g(x: Int) : Int =
sum += x
count += 1
sum / count
end g
count = count + 1
g
end cumulativeAvg
val cavg = cumulativeAvg()
cavg(2) // returns 2
cavg(4) // returns 3
cavg(6) // returns 4This is a closure with mutable captured variables (sum, count), illustrating scope and lifetime directly. Now we apply closures to performance optimization.
3) Decorator pattern with memoization
We first define a function with an observable effect (println) and then wrap it with a cache.
def square(x: Int) : Int =
println(s"$x^2")
x*x
end square
def memoize[X,Y](fn: X=>Y) : X=>Y =
val cache = scala.collection.mutable.Map.empty[X,Y]
x => cache.getOrElseUpdate(x, fn(x))
end memoize
val cachedSquare = memoize(square)
cachedSquare(5)
cachedSquare(5)On the first call, square(5) is computed and printed. On the second call, the value is returned from the captured cache, so the expensive work is skipped.
These examples connect the following themes: closures preserve access to lexical scope, can maintain state across calls, and enable reusable higher-order abstractions.