Functions
Basic Functions
Functions are declared with fn. The body is indented on the next line:
fn add(x: i64, y: i64) -> i64
x + y
No return type means the function returns void:
fn greet(name: String)
print(name)
Short functions can use braces for a single-line body:
fn double(x: i64) -> i64 { x * 2 }
fn apply(x: i64) -> i64 { compute(x) }
Generics
Lowercase type parameters are inferred from usage — no explicit <T> declaration needed on functions:
fn identity(x: t) -> t
x
Multiple type parameters work the same way:
fn pair(left: t, right: u) -> .{ .left: t, .right: u }
.{ .left = left, .right = right }
Effects
Functions that perform side effects declare them via effect rows in the return type:
fn read_file(path: String) -> <io> String
// ...
fn complex(x: i64) -> <io, state> String
// ...
Polymorphic effects use a tail variable:
fn map(xs: List<a>, f: fn(a) -> e b) -> e List<b>
// ...
See Effects & Handlers for the full effect system.
Function Type Parameters
Functions can accept other functions as parameters using fn(Args) -> Ret type syntax:
fn apply(f: fn(i64) -> i64, x: i64) -> i64
f(x)
Visibility and Attributes
Functions are public by default. Use pvt for private:
pvt fn helper() -> i64
42
Attributes precede the function:
#[inline]
fn fast(x: i64) -> i64 { x * 2 }
Everything Combined
#[some_attr]
pvt fn kitchen_sink(x: t, y: u) -> <io, state> Result<t, u> where t: Display + Clone, u: Default
// ...
Lambdas
Anonymous functions use pipe syntax:
let f = || x + 1
let add = |x, y| x + y
let constant = || 42
Lambdas are commonly used with higher-order functions:
items.map(|x| x * 2)
items.filter(|x| x > 0)
items.fold(0, |acc, x| acc + x)
Closure Capture
Closures capture variables from their enclosing scope by copy. The closure gets its own copy of the value at the point of creation:
var x = 10
let f = || x + 1 // f captures a copy of x (10)
x = 99 // x is now 99, but f's copy is still 10
f() // returns 11, not 100
This means closures are self-contained — they don’t hold references to the original variable. Mutating the variable after creating the closure has no effect on the closure’s captured values, and vice versa.
Handler operations are different. Handler ops capture var bindings by reference, because they need live access to shared mutable state:
var cell = 0
with handler<Counter>
ctl get()
resume(cell) // reads the live value of cell
ctl set(x)
cell = x // mutates the original cell
resume(())
This is safe because handlers are scoped constructs — they always run to end of scope and cannot escape. The compiler’s scope coloring analysis verifies this.
| Construct | Capture mode | Can escape? |
|---|---|---|
Closures (|| ...) | Copy (value) | Yes — safe, owns its data |
Handler ops (ctl ...) | By reference | No — scoped to with block |
Arity-Based Dispatch
Multiple functions can share a name if they have different parameter counts:
fn add(a: i32, b: i32) -> i32 { a + b }
fn add(a: i32, b: i32, c: i32) -> i32 { a + b + c }
add(1, 2) // calls add/2
add(1, 2, 3) // calls add/3
The compiler resolves which variant to call by counting arguments. When passed as a value, the expected type disambiguates. See Arity & Config Structs for the full details.
Calling Functions
Standard call syntax:
add(5, 3)
f(g(x))
f(x + y, y * 2)
Method calls on values:
x.len()
x.concat(y)
x.trim().len() // chaining
Path-qualified calls:
Foo::new()
std::io::File::open("test.txt")