13. Closures
A closure is written |params| body. An inline body is a single expression; a trailing-block body, closed by end, is a statement sequence with the same semantics as do…end (§8) - the do is implicit between |x| and end. There is no do/do! keyword to write and no closure-level effect marker - effectfulness is inferred from the body and gated at the call site by the called method's bang.
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
- Inline -
f(|x| EXPR). The closure is an argument inside parentheses (or any value position); the enclosing)or,bounds the single-expression body, so there is noend. The body may sit on the same line, or begin on the next line indented under|params|- it stays one expression either way, so a fold/map step with anif … endbody needs no helper extraction; reach fordo … endwhen 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). - Trailing block -
f |x|then the body on following lines thenend. The closure is a call's last argument, written after the call with no parentheses;endcloses it. The body is ado…endblock (thedoimplicit): a statement sequence whose value is its last expression, or()if it ends on a statement. This suits builder and customization blocks - a sequence ofc.add!(…)calls - and a multi-lineif/match/loopbody reads naturally too.
Effectfulness is inferred, not spelled. A pure combinator (map, find, filter - no bang) checks the closure body in pure context: an action call inside is rejected. An action combinator (each! - has bang) lets the body call actions and propagates their effects to the caller (§6). The bang on the method already says which one applies, so the closure needs no marker of its own.
open io
open list
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 (not in call position) infers its effects from its body, and those effects flow to wherever it is later called. If the binding (or a struct field, or a return type) carries an explicit function type, that type's effect row bounds the closure: an unannotated function type is pure, so storing an effectful closure there requires annotating the row - g: (i32) -> () [io] = |x| io.print!(x) (see §6, Effect-annotated function types).
Parameter types
Closure parameter types are inferred bidirectionally from the expected function type at the call site - typically a higher-order combinator like list.map. No annotation is needed when there is one:
[1, 2, 3].map(|x| x * 2) # x: i32, from List<i32>.map's signature
When 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. Same fix too: annotate either the binding or the parameters.
f: (i32) -> i32 = |x| x * 2
g = |x: i32| x * 2
Body complexity
An inline closure body is exactly one expression - a grammar fact, not a lint: a second statement before the closing ) or , is a parse error. A genuine sequence fits via a do … end block (§8), which is itself one expression. A trailing-block body is already a do…end block, so it sequences directly - no inner do needed; this is the form for builder and customization blocks. Whichever form, keep the logic small: a nested loop or a match with three or more arms anywhere in the body is flagged; extract it into a named def (or def!) so the call site reads as list.map(items, transform) rather than carrying the logic inline. Closures are for one-shot predicates, tiny transformations, and short builder sequences; once the logic grows past that, the naming pressure of def pays off.
No brace-block closures. Ruby's { |x| x*2 } is not allowed in Hanki - Ruby's precedence footgun is well known. |x| body is the only form.