Docs/Language fundamentals

Functions & closures

Define a useful interface, return results, and pass behavior to other code.

On this page

A function declares the types it accepts and the values it returns. Local variables inside its body can still use type inference.

Define and call a function#

Valk
fn line_total(price: int, quantity: int (1)) int {
    return price * quantity
}

fn main() {
    println(line_total(12))    // 12
    println(line_total(12, 3)) // 36
}

The result type follows the parameter list. A function that only performs an action can omit its result type. A default argument goes in parentheses after its type, as in quantity: int (1).

Arguments are evaluated from left to right after the callable or method receiver. Prefer straightforward calls when arguments also change state.

Read command-line arguments#

Declare an Array[String] parameter on main to receive arguments:

Valk
fn main(args: Array[String]) {
    let name = args.get(1) !? "Guest"
    println("Welcome, %name")
}

Index zero is the executable's path. This program uses the first user-supplied argument as a name, or Guest if it is absent.

Pass a function#

A callback type uses fn(argument-types)(return-types). Here, fn(int)(int) accepts an integer and returns an integer:

Valk
fn apply_twice(value: int, transform: fn(int)(int)) int {
    return transform(transform(value))
}

fn main() {
    let increment = fn(value: int) int { return value + 1 }
    println(apply_twice(3, increment)) // 5
}

An anonymous function uses the same fn syntax as a named one, but has no name. Its parameter and return types must match the callback signature; use a wrapper function when a conversion is needed.

Capture surrounding values#

A closure can use a value from the scope where it is created:

Valk
fn main() {
    let prefix = "Hello"
    let greet = fn(name: String) String {
        return "%prefix, %name"
    }
    prefix = "Goodbye"
    println(greet("Ada")) // Hello, Ada
}

The closure captures the current value when it is created. Reassigning prefix later does not update the captured value. Capturing an object captures a reference to that object, so mutations to the object can still be shared. See Memory & references.

Run cleanup before returning#

defer schedules a call for the end of the current function. Put it immediately after acquiring a resource so the cleanup is visible next to the acquisition:

Valk
let file = fs.stream("notes.txt") ! return
defer file.close() _

Deferred calls run in reverse order on a normal return or an error return. Their arguments are captured when the defer statement is reached. The _ above deliberately ignores a close error; when successful output matters, close explicitly and handle the result.

To declare a function that can fail, add an error type after its result type. That is covered in Error handling.