Functions
Defining functions
fn add(a, b) {
return a + b
}
print(add(2, 3)) # 5
A function with no return returns nil. return with no value also
returns nil.
Anonymous functions
Functions are first-class values:
var doubler = fn(n) {
return n * 2
}
print(doubler(21)) # 42
var square = fn(x) { return x * x }
print(square(5)) # 25
Arguments
Arguments are passed by value (deep-copied): mutating a parameter never
touches the caller’s variable. To share or mutate outside state, pass a ref
(see Syntax — ref).
Missing arguments default to nil; too many arguments is an error:
fn add(a, b) { return a + b }
print(add(1, 2, 3)) # error: too many arguments
Missing arguments default to nil:
fn greet(name, title) {
return title + " " + name
}
print(greet("Smith")) # nil Smith
print(greet("Smith", "Dr")) # Dr Smith
Functions as data
var ops = {
add: fn(a, b) { return a + b },
mul: fn(a, b) { return a * b }
}
print(ops.add(10, 3)) # 13 (dot notation for members)
print(ops.mul(4, 5)) # 20
Discarded return values
Some standard-library functions are pure: they have no side effects, so
their return value is the entire point (std.string.upper,
std.math.abs, len, std.hash.md5, …). Calling one as a bare statement
silently throws the result away, which is almost certainly a bug — the
engine does not warn (a discard lint is on the roadmap):
std.string.upper("hi") # does nothing — the uppercased string is lost
Use the result instead:
var loud = std.string.upper("hi") # assigned: kept
print(std.math.abs(0 - 5)) # passed along: kept
Dropping a value is only legitimate for effectful calls (print,
std.async.spawn, …). Note insert/erase are not effectful: the bare
builtins are pure (assign the result or use the : pipe form), so a
discarded insert(...) is silently lost just like the example above.
Returning multiple values
A function returns a single value, but that value can be an array, and a
destructuring declaration unpacks it into several variables in one step:
fn divmod(a, b) {
return [a / b, a % b]
}
var c, e = divmod(17, 5) # c is 3, e is 2
print(c, e) # 3 2
fn stats(list) {
var total = 0
for (v : list) { total += v }
return [total / len(list), total]
}
var avg, sum = stats([10, 20, 30, 40]) # avg 25, sum 100
print(avg, sum) # 25 100
Rules for unpacking an array:
var a, b = [1, 2] # a is 1, b is 2
# var x, y = [1] # error: not enough elements
You cannot destructure an object
Destructuring works on arrays only. Returning an object and trying to
unpack it as a pair is an error:
fn get_pair() {
return {this: "value"}
}
var v, c = get_pair()
# error: destructuring requires an array value, got object
To return named values, return an object and access it by key:
fn get_pair() {
return {v: 3, c: 5}
}
var pair = get_pair()
print(pair.v, pair.c) # 3 5
Each element of the pattern must be a plain name — there are no nested or
ref patterns.
This is the idiomatic way to return “multiple values” — the function returns
an array, the caller destructures it. The destructuring declares var /
const / local bindings (but not ref, which cannot destructure), and it
works on any array-valued expression, not just function calls.
Higher-order functions work naturally:
fn apply_twice(f, x) {
return f(f(x))
}
print(apply_twice(square, 2)) # 16
Recursion
Recursion depth is capped at 256 per execution context; exceeding it
raises a clean STACK_OVERFLOW error.
fn fact(n) {
if (n <= 1) { return 1 }
return n * fact(n - 1)
}
print(fact(6)) # 720
Globals, not lexical closures
Functions read and write global variables directly, and there is
no lexical capture of enclosing locals:
var counter = 0
fn increment() {
counter++
}
increment()
print(counter) # 1
fn broken() {
var inner = 42
return fn() { return inner } # error: inner is not visible here
}
This matters for callbacks and async tasks — reach data through globals or
pass it as arguments.
Scope summary
- Top-level
var/constdeclarations are global. var/constinside a function are local to that call.- Blocks (
if/while/for/else, standalone{}) create new scopes;
variables declared inside are local to that block. - For-loop init/condition/increment run in the enclosing scope.
- There are no lexical closures — functions see only globals, not enclosing
locals (see Globals, not lexical closures).