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 an
alias — &state (see Syntax — &).

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
patterns.

This is the idiomatic way to return “multiple values” — the function returns
an array, the caller destructures it. The destructuring declares var /
const bindings, 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

Closures: capture and scope

Functions read and write global variables directly, and inner
functions capture enclosing locals:

var counter = 0
fn increment() {
  counter++
}
increment()
print(counter)   # 1

Globals are read live — the function sees the current value each time
it runs. Enclosing locals and parameters are captured by value at the
moment the fn literal is evaluated; later writes to the outer variable
are invisible to the closure:

fn outer() {
  var secret = 7
  var f = fn() { return secret }
  secret = 99
  return f
}
print(outer()())   # 7 — captured when the fn literal ran
fn mk(prefix) {
  return fn(name) { return prefix + ":" + name }
}
var greet = mk("hello")
print(greet("world"))   # hello:world

Captures are per evaluation: two calls to mk bake two independent
frames (see Comparing functions).

This makes closures the way to reach local state from callbacks and async
tasks — capture what you need when you build the callback, or pass it as
an argument.

Loop headers follow the same rule. A C-style for header variable is
scoped to the loop, and each iteration’s fn captures the value it saw
when the literal was evaluated — so the classic “make a function per
counter value” just works:

var fns = []
for (var i = 0; i < 3; i++) {
  fns += fn() { return i }
}
print(fns[0](), fns[1](), fns[2]())   # 0 1 2 — one captured `i` per iteration

See Control Flow — For (C-style) for the
loop-side view.

Comparing functions

Functions are honest values. A copy equals its original, natives compare
by identity, and two script closures compare structurally: same body
(compared through the AST — whitespace and positions don’t matter), same
parameters, and deep-equal captures:

var f = fn() { return 1 }
print(f == fn() { return 1 })   # true  — same code
var mk = fn(p) { return fn() { return p } }
print(mk(1) == mk(1))           # true  — same code, equal captures
print(mk(1) == mk(2))           # false — captures differ

Comparing a script closure to a native function is always false.
Structural comparison is cycle-safe: closures whose captures reference
themselves compare without recursing forever.

Scope summary

One rule covers it: var/const live in the block where you wrote them
— at the top level, that block is the whole script, so top-level
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. A C-style for header
    variable is scoped to the loop — it does not exist after the loop ends.
  • Closures capture enclosing locals by value when the fn literal is
    evaluated — including the for header variable, captured fresh per
    iteration
    — while globals are read live (see
    Closures: capture and scope and
    Control Flow — For).