Functions & Closures

Functions are declared with fn. Parameters must be annotated; the return type follows -> and defaults to () when omitted.

fn greet(name: str) {
    print(f"hello, {name}!");
}

fn square(n: int) -> int {
    n * n
}

The body is a block, so its final bare expression is the return value. The annotated return type is enforced:

fn oops() {
    0 // compile error: expected (), found int
}

return exits early and, being never-typed, fits anywhere:

fn clamp_low(n: int) -> int {
    if n < 0 {
        return 0;
    }
    n
}

Optional parameters

A parameter with a default value becomes optional. Like a let, its type can be inferred from the default, so the annotation is optional too.

fn connect(host: str, port=8080) {
    // ...
}

connect("localhost");        // port defaults to 8080
connect("localhost", 9090);  // or pass it explicitly

Defaults must be const-foldable, and every optional parameter has to come after all the required ones.

fn bad(a: int, b=0, c: int) {} // compile error: a required parameter can't follow an optional one
fn good(a: int, c: int, b=0) {} // valid

Named arguments

Optional arguments may be passed by name — again, only once all the required positional arguments are supplied. Naming lets you skip over defaults you don’t care about and set one further along:

fn style(text: str, bold=false, italic=false, size=12) {}

style("hi", size=18, bold=true); // skip `italic`, set the other two

Closures

A closure is an inline, anonymous function written with pipes. Its return type is optional and inferred when left off.

let add = |a: int, b: int| -> int { a + b };
let double = |n: int| { n * 2 }; // return type inferred

let total = add(double(3), 4); // 10

A no-argument closure uses an empty pair of pipes:

let now = || current_time();

Note

Functions cannot currently be passed to native functions and called. This is a high priority to be added after v0.1.0.

Closures vs. functions

The two differ in two important ways.

First- vs. second-class. Closures are first-class: assign them to bindings, store them in arrays, pass them around freely. Functions are second-class — you can pass one as an argument, but you can’t bind or store it as a value.

let add = |a: int, b: int| -> int { a + b };
fn sub(a: int, b: int) -> int { a - b }

apply(add); // legal — passing a closure
apply(sub); // legal — passing a function as an argument is fine

let f = add; // legal
let g = sub; // compile error: a function isn't a value you can bind

Capturing. A closure can capture variables from the scope around it. A function cannot — it sees only its parameters and top-level items.

let greeting = "hello!";

let say = || print(greeting); // legal — captures `greeting`

fn say_fn() {
    print(greeting); // compile error: `greeting` is undefined in here
}

Functions live at the top level

fn declarations are only allowed at the root of a file. Need a function-like value deeper inside another function or a block? That’s exactly what closures are for.

fn outer() {
    fn inner() {}      // compile error: no nested functions
    let inner = || {}; // do this instead
}