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/const declarations are global.
  • var/const inside 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).