Anonymous Functions and Closures
The Spellings
An anonymous function has two halves you can vary independently: how it opens, and how its body is written. That gives a small grid rather than a list to memorise.
Opening. def is the keyword; @ is its shorthand. They are the same
thing.
Body. A block in braces returns with return. An arrow returns the one
expression after it.
var block_long = def(x) { return x * x }
var block_short = @(x) { return x * x }
var arrow_long = def(x) => x * x
var arrow_short = @(x) => x * x
echo [block_long(3), block_short(3), arrow_long(3), arrow_short(3)]
[9, 9, 9, 9]
When there are no parameters, the empty parentheses may be dropped as well:
var a = @() { return 1 }
var b = @() => 2
var c = def() { return 3 }
var d = def() => 4
var e = @ => 5
var f = @{ return 6 }
var g = def { return 7 }
var h = def => 8
echo [a(), b(), c(), d(), e(), f(), g(), h()]
[1, 2, 3, 4, 5, 6, 7, 8]
All eight are the same construct. Which to use:
@(x) => exprfor a one-expression callback. This is what most Zuri code uses, and what the standard library is written in.@(x) { ... }when the body needs more than one statement.def(x) { ... }when the function is long enough that you want it to look like the declarations around it.@ => exprfor a thunk — a value computed on demand, with nothing passed in.
Compare the two common ones where they actually turn up:
echo [' a ', ' b'].map(@(t) => t.trim())
echo [1, 2, 3].filter(@(n) {
if n == 2 {
return false
}
return n > 0
})
[a, b]
[1, 3]
The arrow form has no return because the expression is the return
value. Adding one is a mistake the compiler will not catch: @(x) => return x
does not parse, but @(x) { x } parses and returns nil.
Anonymous functions take variadic parameters and type annotations exactly like named ones:
var sum = @(...numbers) => numbers.reduce(@(a, b) => a + b, 0)
var doubled = @(n: number) => n * 2
echo sum(1, 2, 3)
echo doubled(4)
catch {
doubled('four')
} as e {
echo e.message.replace('/@anon\d+/', '@anonN')
}
6
8
@anonN() expects parameter 'n' (argument 1) to be a number, got string
The real message carries a number rather than the N shown here. An
anonymous function is named @anonN in the order the compiler met it in
the file, which is worth knowing when one turns up in a stack trace — and
worth not depending on, since inserting another anonymous function above it
renumbers everything below.
Closures
A function carries the variables it referenced from its surroundings, and keeps them alive after the enclosing function has returned:
def make_counter() {
var n = 0
return @() {
n++
return n
}
}
var a = make_counter()
var b = make_counter()
echo a()
echo a()
echo b()
1
2
1
a and b each closed over their own n. Each call to
make_counter() created a fresh one.
Capture Is by Reference
A closure captures the variable, not a snapshot of its value:
def demo() {
var total = 0
var add = @(n) { total += n }
add(5)
add(10)
return total
}
echo demo()
15
This is what makes accumulator patterns work, and it is what makes loop variables surprising. If you want a per-iteration capture, declare a fresh variable inside the loop body:
var fns = []
iter var i = 0; i < 3; i++ {
var captured = i
fns.append(@() => captured)
}
echo fns.map(@(f) => f())
[0, 1, 2]
captured is a new variable on each pass, so each closure gets its own.
Closures over Parameters
Parameters are captured the same way, which is the basis of partial application:
def adder(amount) {
return @(n) => n + amount
}
var add_five = adder(5)
var add_ten = adder(10)
echo add_five(1)
echo add_ten(1)
6
11
Recursion in an Anonymous Function
An anonymous function has no name to call itself by. Bind it first:
var factorial
factorial = @(n) {
if n <= 1 {
return 1
}
return n * factorial(n - 1)
}
echo factorial(5)
120
The variable is captured by reference, so by the time the body runs,
factorial is bound.
Functions Compare by Identity
def f() {}
var g = f
echo f == g
echo f == @() {}
true
false
Two separately written functions are never equal, even with identical bodies.
A Worked Example
Closures are at their most useful when a function needs to remember something between calls without that something becoming a global. Here is a rate limiter: it hands back a function that answers “may I do this now?”, and keeps its own tally where nothing else can reach it.
def make_limiter(max_per_window: number) {
var used = 0
return {
allow: @() {
if used >= max_per_window {
return false
}
used++
return true
},
remaining: @() => max_per_window - used,
reset: @() {
used = 0
},
}
}
var limiter = make_limiter(2)
var allow = limiter.allow
var remaining = limiter.remaining
var reset = limiter.reset
echo allow()
echo allow()
echo allow()
echo remaining()
reset()
echo allow()
true
true
false
0
true
Three separate functions share one used, because all three closed over
the same variable in the same call to make_limiter. A second call to
make_limiter would produce a second, independent trio.
This is the closest Zuri gets to a private field without a class, and it is
worth knowing for exactly that reason: there is no way for a caller to read
or write used except through the three functions you gave them.