Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

ConstructCapture modeCan escape?
Closures (|| ...)Copy (value)Yes — safe, owns its data
Handler ops (ctl ...)By referenceNo — 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")