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()) -- 3Nested 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()) -- 30Mutual 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)) -- trueFunction 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