Functions

Named functions, lambdas, closures, overloading, generics, and function attributes.

Summary

The last expression in a body is the implicit return value. fn double(x) = x * 2 is a shorthand for single-expression bodies. Default parameters and variadic (...args) are supported. Functions with the same name but different arities coexist as overloads; the first exact-arity match wins. Closures capture by reference and mutations are visible to the outer scope. Type parameters use <T>; trait bounds use T: Trait. Attributes like @test, @deprecated, and @scoped control static analysis behavior. fn main() is auto-called if defined.

Canonical

-- basic function
fn greet(name) {
    println("Hello, {name}!")
}

-- with return value
fn add(a, b) {
    return a + b
}

-- expression-body shorthand
fn double(x) = x * 2

-- implicit return (last expression in body)
fn square(x) { x * x }

-- with type annotations
fn factorial(n: int) -> int {
    if n <= 1 { return 1 }
    return n * factorial(n - 1)
}

-- lambda / anonymous function
let sq = fn(x) { x * x }

-- arrow lambda
let inc = (x) => x + 1

-- default parameters
fn greet(name, greeting = "hello") {
    return "{greeting}, {name}"
}
println(greet("world"))          -- hello, world
println(greet("world", "hi"))    -- hi, world

-- variadic
fn sum(...args) {
    var total = 0
    for a in args { total = total + a }
    return total
}
println(sum(1, 2, 3))           -- 6
println(sum())                   -- 0

-- functions as values
fn apply(f, x) { return f(x) }
println(apply(fn(x) { x * 3 }, 5))  -- 15

-- fn main() is auto-called if defined
fn main() {
    println("entry point")
}

Implicit return applies to the last expression in the block. For early returns, use return explicitly.

Function Attributes

Decorate functions with @ or #[...] attributes before the fn keyword.

-- @pure: checked by sema: cannot call println, read_file, sleep, etc.
@pure
fn add(a, b) { return a + b }

-- @test: marks a function as a test case (used by xs test)
@test
fn test_math() {
    assert_eq(1 + 1, 2)
}

-- @deprecated: warns callers at check time
@deprecated("use new_api() instead")
fn old_api() { return 42 }

-- #[...] attribute syntax (equivalent)
#[test]
fn test_strings() {
    assert("hello".len() == 5)
}

#[deprecated("moved to v2")]
fn legacy() { }

-- pub: marks a function as public (visible to importers)
pub fn helper() { return 42 }

-- static: in impl blocks, a method that doesn't take self
impl Point {
    static fn origin() {
        return Point { x: 0, y: 0 }
    }
}

-- @[macro]: marks a fn as a procedural macro. the body runs at
-- the call site and returns a transformed value. examples in
-- examples/macros/ (derive_eq, derive_show, sql).
@[macro]
fn show_for(name) {
    return fn(v) { return name + " " + str(v) }
}

@pure is enforced by the semantic analyzer: calling impure functions (I/O, sleep, exit) inside a @pure function is an error.

@scoped bindings

@scoped annotates a let or var whose value must not outlive the lexical block that introduced it. Run with --check or --strict and the semantic analyzer rejects the obvious escape patterns (returning the binding, storing it in a non-scoped container, capturing it in a closure that survives the scope). The runtime opts the value out of cycle detection because refcounting alone suffices once escape is statically forbidden.

fn process() {
    @scoped let buf = make_buffer(64 * 1024)
    return buf.checksum()  -- ok: only the scalar checksum escapes
}

fn leak() {
    @scoped let buf = make_buffer(64 * 1024)
    return buf            -- xs --check: error S0042: scoped binding escapes
}

Generic type parameters

Function, struct, and enum declarations take optional <T, U> type parameter lists. Each parameter can carry a variance marker (+T covariant, -T contravariant, default invariant) and an optional trait bound (T: Show). forall<T> body is also legal as a type expression for a higher-rank polymorphic value.

struct Box<+T>  { inner }            -- covariant
struct Sink<-T> { accept }            -- contravariant

fn first<+T>(xs) {
    if len(xs) > 0 { return xs[0] }
    return null
}

fn run(f: forall<T> fn(T) -> T, x) {
    return f(x)
}

Closures

Closures capture variables by reference through an environment chain. Mutations inside a closure are visible to the outer scope.

fn make_counter() {
    var count = 0
    return fn() {
        count = count + 1
        return count
    }
}
let c = make_counter()
println(c())                     -- 1
println(c())                     -- 2
println(c())                     -- 3

Nested closures work: each level captures its parent's environment:

fn outer() {
    var x = 10
    fn middle() {
        var y = 20
        fn inner() { return x + y }
        return inner()
    }
    return middle()
}
println(outer())                 -- 30

Mutual Recursion

Functions can call each other before both are defined (they're hoisted):

fn is_even(n) {
    if n == 0 { return true }
    return is_odd(n - 1)
}
fn is_odd(n) {
    if n == 0 { return false }
    return is_even(n - 1)
}
println(is_even(10))             -- true
println(is_odd(7))               -- true

Function Overloading

Multiple functions with the same name are dispatched by argument count at call time.

fn greet() {
    println("hello, world!")
}

fn greet(name) {
    println("hello, {name}!")
}

fn greet(name, greeting) {
    println("{greeting}, {name}!")
}

greet()                          -- hello, world!
greet("Alice")                   -- hello, Alice!
greet("Bob", "hey")             -- hey, Bob!

Overloading works with default parameters and variadic functions. When multiple overloads could match, the first one with an exact arity match wins. If no overload matches the argument count, it's a runtime error.

fn calc(x) = x * 2
fn calc(x, y) = x + y
fn calc(x, y, z) = x + y + z

println(calc(5))                 -- 10
println(calc(3, 4))              -- 7
println(calc(1, 2, 3))           -- 6