13. Closures
The reasons for the rules of this section are in docs/design/closures.md.
A closure is written |params| body, in one of two layouts. An inline body is a single expression. A trailing-block body, closed by end, is a statement sequence with the semantics of do…end (§8), the do being implicit between |x| and end. There is no do or do! keyword to write.
Two layouts, one closure:
use io
def main!() -> () [io]
[1, 2, 3].map(|x| x * 2) # inline: the `)` bounds the body, no `end`
[1, 2, 3].each! |x| # trailing block: the `end` closes it
io.print!("#{x}\n")
end
end
An inline closure, f(|x| EXPR), appears as an argument inside parentheses or in any other value position, and the enclosing ) or , bounds its single-expression body. There is no end. The body may sit on the same line, or begin on the next line indented under |params|. It is one expression in both layouts, and a fold or map step with an if … end body needs no helper extraction. Use do … end where a sequence is wanted. Use the inline form for a closure that is not the call's last argument too: apply(|n| n * 2, 5).
def apply(f: (int) -> int, n: int) -> int
f(n)
end
def ten() -> int
apply(|n| n * 2, 5)
end
def evens(xs: List<int>) -> int
xs.fold(0, |acc, x|
if x % 2 == 0
acc + 1
else
acc
end)
end
A trailing-block closure, f |x| then the body on following lines then end, is a call's last argument, written after the call with no parentheses, and end closes it. The body is a do…end block with the do implicit: a statement sequence whose value is its last expression, or () where it ends on a statement. This suits builder and customization blocks, a sequence of c.add!(…) calls, and a multi-line if or match within the limits of Body complexity.
use io
def main!() -> () [io]
[1, 2].each! |x|
y = x * 10
io.print!("#{y}\n")
end
end
A trailing-block closure fills the first parameter, in declaration order, that the parenthesized arguments leave unfilled; for a callee without defaults that is the closure parameter. The closure may follow keyword arguments, sum_upto(xs, limit=2) |x| then the body then end, and fills the first parameter they leave unfilled.
def sum_upto(xs: List<int>, limit: int = 3, weight: (int) -> int = |x| x) -> int
xs.take(limit).fold(0, |acc, x| acc + weight(x))
end
def heavy() -> int
sum_upto([1, 2, 3, 4], limit=2) |x|
x * 10
end
end
test "a trailing block after a keyword argument fills the parameter it leaves unfilled"
assert!(heavy() == 30)
assert!(sum_upto([1, 2, 3, 4]) == 6)
end
hanki fmt prints the closure of a call that has a keyword argument in the form it was written.
A closure has no effect marker of its own. Effectfulness is inferred from the body and gated at the call site by the called method's bang. A pure combinator (map, find, filter, with no bang) checks the closure body in pure context, and an action call inside is rejected (H0606). An action combinator (each!, with a bang) lets the body call actions and propagates their effects to the caller (§6).
use io
def demo!(xs: List<i32>) -> () [io]
xs.map(|x| io.print!("#{x}")) # error: map is pure; its closure cannot call an action
xs.each!(|x| io.print!("#{x}")) # ok: each! is an action; [io] propagates to the caller
end
A let-bound closure, one not in call position, infers its effects from its body, and those effects flow to wherever it is later called. Where the binding, a struct field or a return type declares an explicit function type, that type's effect row bounds the closure. An unannotated function type is pure (§6), and storing an effectful closure there requires annotating the row: g: (i32) -> () [io] = |x| io.print!("#{x}"). A closure stored under a function type whose row does not permit its effects is H0524.
use io
def main!() -> () [io]
show = |x: i32| io.print!("#{x}\n")
show(1i32)
g: (i32) -> () [io] = |x| io.print!("#{x}\n")
g(2i32)
end
Without the row, the binding is refused:
use io
def main!() -> () [io]
g: (i32) -> () = |x| io.print!("#{x}\n")
g(1i32)
end
Capture
A closure captures the bindings it uses by value. It copies them where it is written, and the closure and the enclosing scope never share a mutable cell: nothing the closure does is visible through a captured name afterwards, and nothing the enclosing scope does later changes what the closure sees.
def snapshot() -> int
var n = 1
get = || n
n = 2
get()
end
test "a closure sees the value it captured where it was written"
assert!(snapshot() == 1)
end
A flow-narrowing fact established where a closure is written applies inside its body. Under an if y != 0 guard, a closure body divides by y with a bare / (§3, Division).
def scaled(xs: List<i32>, y: i32) -> List<i32>
if y != 0
xs.map(|x| x / y)
else
xs
end
end
Assigning to a captured binding is a compile error (H0209).
use io
def main!() -> () [io]
var total = 0
[1, 2, 3].each! |x|
total = total + x # error: `total` was captured by value
end
io.print!("#{total}\n")
end
Two forms give what that reaches for. A loop in the enclosing body, where the var is a real local and the write is an ordinary one; or a fold, which threads the accumulator through and returns it:
def sum(xs: List<int>) -> int
xs.fold(0, |acc, x| acc + x)
end
The rule bites only on captured bindings. A closure's own parameters and its own var locals are ordinary mutable locals, writable as usual and from a nested block inside the body, and a parameter that shadows a captured name is the closure's own binding too.
def clamped_sum(xs: List<int>) -> int
xs.fold(0, |acc, x| do
var step = x
if step < 0
step = 0
end
acc + step
end)
end
def bumped(total: int, xs: List<int>) -> List<int>
xs.map(|total| do
total = total + 1
total
end)
end
test "a parameter that shadows a captured name is the closure's own binding"
assert!(bumped(100, [1, 2]) == [2, 3])
assert!(clamped_sum([3, -1, 2]) == 5)
end
A field write rebinds its root (§12), and c.v = x through a captured c is rejected with H0209 on the same grounds as a bare write to c.
use io
struct Cell
v: int
end
def main!() -> () [io]
var c = Cell(v=0)
[1, 2].each! |x|
c.v = x
end
io.print!("#{c.v}\n")
end
A resource is the one capture that is not copied. A closure that uses a resource captures the handle itself, and the closure and the enclosing scope then act on one resource: the handle remains usable in the enclosing scope after the closure runs, and both read and write through it.
use sqlite
open result
def kept!(conn: SqliteConn, ids: List<int>) -> List<int> [db]
ids.filter!(|i| match conn.query!("SELECT ?", [sqlite.v(i)])
Ok(_) -> true
Err(_) -> false
end)
end
test "a closure captures a connection, which remains usable afterwards"
Ok(conn) = sqlite.open_memory!() else Err(_) -> crash!("no database")
assert!(conn.batch!("CREATE TABLE t (x INTEGER)") == Ok(()))
assert!(kept!(conn, [1, 2, 3]).length == 3)
assert!(conn.execute!("INSERT INTO t VALUES (1)", []) == Ok(1))
assert!(kept!(conn, [4]).length == 1)
end
The predicate above queries conn and leaves the handle open for the enclosing scope to keep using. Two rules stated elsewhere also apply to a captured resource. A send copies the closure with its captures, and a captured resource or Future faults the copying actor (§15). A capture taken before a move hands the resource away still names the tombstoned slot, and using it faults the actor on both tiers with resource was moved to another actor (§4).
The pure and action forms both remain
A combinator that takes a callback comes in two forms, and both are part of the surface for good: the pure map/filter/fold/find/find_map/any?/all?/flat_map/partition/take_while/count, and an effect-polymorphic ! twin for each alongside them. The ! distinguishes them at the call site, and the pure ones are not deprecated. Only the pure form is usable from a pure def, a where block or meta.
use io
def doubled(xs: List<int>) -> List<int>
xs.map(|x| x * 2)
end
def logged!(xs: List<int>) -> List<int> [io]
xs.map! |x|
io.print!("#{x}\n")
x * 2
end
end
The pure form rejects an effectful callback: an action call in the callback is H0606, and any other effect is H0603. Use the ! form where the callback needs an effect, and the pure one otherwise.
def applied(xs: List<int>, f: (int) -> int [io]) -> List<int>
xs.map(|x| f(x))
end
The rule reaches past List. Option and Result are the other callback-taking types in core, and their combinators are paired the same way: option.map!/and_then!/or_else! and result.map!/map_err!/and_then!/or_else! (§7).
use io
def shown!(x: Option<int>) -> Option<int> [io]
x.map! |v|
io.print!("#{v}\n")
v
end
end
def checked!(r: Result<int, string>) -> Result<int, string> [io]
r.map_err! |e|
io.print!("#{e}\n")
e
end
end
sort_by, min_by and max_by are pure-only, and there is no sort_by!. Compute whatever the effect would supply into the elements first (xs.map!(…)), then sort the result with a pure comparator.
def shortest_first(words: List<string>) -> List<string>
words.sort_by(|a, b| a.length < b.length)
end
def longest(words: List<string>) -> Option<string>
words.max_by(|a, b| a.length < b.length)
end
Parameter types
Closure parameter types are inferred bidirectionally from the expected function type at the call site, usually that of a higher-order combinator such as map. No annotation is needed where there is one:
[1, 2, 3].map(|x| x * 2) # x: int, from List<int>.map's signature
Where there is no expected type, a closure bound to an unannotated let with no other constraint, inference cannot pin the parameters, and the compiler reports cannot infer type, the same diagnostic as §11. The fix is the same: annotate either the binding or the parameters.
f: (i32) -> i32 = |x| x * 2
g = |x: i32| x * 2
Function values of defaulted callables
A callable that declares defaults can be taken as a value. The expected function type selects the arity the value presents, from the callable's required count to its full count, and with no expected function type the value presents the required count. An expected count outside that range is H0533.
def greet(name: string, greeting: string = "hello") -> string
"#{greeting}, #{name}"
end
test "a defaulted callable presents the arity its expected type selects"
a = greet # (string) -> string
b: (string, string) -> string = greet # full arity
assert!(a("ana") == "hello, ana")
assert!(b("ana", "hi") == "hi, ana")
assert!(["ana", "bo"].map(greet) == ["hello, ana", "hello, bo"])
end
def greet(name: string, greeting: string = "hello") -> string
"#{greeting}, #{name}"
end
def three() -> int
c: (string, string, string) -> string = greet
1
end
A value of reduced arity behaves as a closure written at the reference: each call through it evaluates the omitted defaults, in the callable's declaring scope with the earlier parameters bound. It captures what the reference site supplies, the dictionaries of a bounded callable included. A full-arity value is the function itself.
def tag(name: string, label: string = name.to_ascii_uppercase()) -> string
"#{label}:#{name}"
end
test "a reduced-arity value evaluates the omitted default on each call"
f: (string) -> string = tag
assert!(f("a") == "A:a")
assert!(f("b") == "B:b")
end
Body complexity
An inline closure body is one expression. This is a grammar fact: a second statement before the closing ) or , is a parse error (H0101). A sequence fits through a do … end block (§8), which is itself one expression. A trailing-block body is already a do…end block and sequences directly, with no inner do needed.
def adjusted(xs: List<int>) -> List<int>
xs.map(|x| do
y = x * 2
y + 1
end)
end
def adjusted(xs: List<int>) -> List<int>
xs.map(|x| y = x * 2
y + 1)
end
In either form, a loop, or a match with three or more arms, anywhere in a closure body is a compile error (H0531). Extract it into a named def or def! and pass that: the call site becomes items.map(label), with the logic named.
def labels(xs: List<int>) -> List<string>
xs.map(|n| match n
0 -> "zero"
1 -> "one"
_ -> "many"
end)
end
def label(n: int) -> string
match n
0 -> "zero"
1 -> "one"
_ -> "many"
end
end
def labels(items: List<int>) -> List<string>
items.map(label)
end
There are no brace-block closures. { |x| x * 2 } does not lex (H0001), and |x| body is the only form.
def doubled(xs: List<int>) -> List<int>
xs.map({ |x| x * 2 })
end