Skip to content

Functions and closures

A function declaration needs a type on every parameter, and a return type if it returns a value. There are no default arguments, no named arguments and no variadic functions.

uses print
fn makeAdder(base: Int): fn(Int): Int {
return fn(n: Int): Int {
return base + n
}
}
fn apply(f: effect fn(String), text: String) {
effect f(text)
}
constant addFive = makeAdder(5)
effect print("${addFive(1)} ${addFive(10)}")
apply(print, "direct call")
apply(
fn(s) {
effect print("wrapped: ${s}")
},
"hi"
)

Output:

6 15
direct call
wrapped: hi

makeAdder returns a closure, an anonymous function value that reads base from the scope it was created in. A closure parameter can often skip its own type annotation, as s does above, when the expected function type is already known from context, here the f: effect fn(String) parameter of apply. See section 5.3.

apply’s first parameter is effect fn(String), an effectful function type, so the body has to mark its own call with effect, and a caller can hand it either a real capability (print) or a closure that happens to call one internally. A plain fn(String) parameter could not accept print at all: pure and effectful are different types, and only pure is assignable to effectful, never the other way round. That is how a function signature tells you, just by its type, whether it can possibly reach the outside world. See section 5.5 and the effects chapter.

A closure shares its captured bindings with the code around it: it sees them by reference, not a copy taken when the closure was created. A closure that assigns a variable changes what the enclosing code, and every other closure over that binding, sees:

uses print
fn makeCounter(): fn(): Int {
variable n = 0
return fn(): Int {
n = n + 1
return n
}
}
constant next = makeCounter()
effect print("${next()} ${next()} ${next()}")

Output:

1 2 3

Each execution of a declaration creates a new binding, though, so a closure created inside a loop does not share the loop variable with closures from other iterations:

uses print
variable makers: [fn(): Int] = []
for i in range(0, 3) {
makers = makers.append(fn(): Int {
return i * 10
})
}
constant results = makers.map(fn(make) {
return make()
})
effect print(results.map(fn(n) {
return "${n}"
}).join(","))

Output:

0,10,20

If i were shared across iterations, this would print 20,20,20. See section 5.4 for the precise rule, including what a closure is and is not allowed to assign.