Skip to content

Functions

Define functions with the fn keyword.

fn greet() {
print("Hello, RESONON!");
}
greet();
fn add(a, b) {
return a + b;
}
print(add(3, 4)); // 7

Use return to exit a function early.

fn absolute(x) {
if x < 0 { return -x; }
return x;
}
fn grade(score) {
if score >= 90 { return "A"; }
if score >= 80 { return "B"; }
if score >= 70 { return "C"; }
return "F";
}

The last expression in a function body is its return value when no explicit return is used.

fn square(x) {
x * x
}
print(square(5)); // 25

Functions can return patterns just like any other value.

fn c_major() {
return [C4 E4 G4];
}
print(c_major());

Assign a function literal to a variable.

let double = fn(x) { return x * 2; };
print(double(7)); // 14
let is_even = fn(n) { return n % 2 == 0; };
print(is_even(4)); // true

Anonymous functions capture their enclosing environment at creation time.

let multiplier = 10;
let times_ten = fn(x) { return x * multiplier; };
print(times_ten(5)); // 50

Capture is by value — changing the outer variable after creation does not affect the closure:

let base = 100;
let get_base = fn() { return base; };
base = 999;
print(get_base()); // 100 (still the original value)
fn make_adder(n) {
return fn(x) { return x + n; };
}
let add5 = make_adder(5);
let add10 = make_adder(10);
print(add5(3)); // 8
print(add10(3)); // 13
fn make_linear(m) {
return fn(b) {
return fn(x) { return m * x + b; };
};
}
// Apply each returned function directly — calls chain left to right.
print(make_linear(2)(3)(0)); // y = 2x + 3 at x=0 -> 3
print(make_linear(2)(3)(5)); // 13
// Or bind an intermediate step and reuse it:
let line = make_linear(2)(3); // y = 2x + 3
print(line(0)); // 3
print(line(5)); // 13

Closures only capture variables they actually reference, not the entire enclosing scope.

let x = 10;
let y = 20;
let use_x = fn() { return x; }; // captures only x
let use_y = fn() { return y; }; // captures only y
print(use_x()); // 10
print(use_y()); // 20

Anonymous functions can be passed directly as arguments without assigning them to a variable first.

fn apply(func, value) {
return func(value);
}
print(apply(fn(x) { return x * x; }, 5)); // 25

This pattern is used extensively with array methods like .map() and .filter() — see Arrays and the Higher-order functions section below.

Name resolution in Resonon is deliberately simple: a function’s meaning depends only on its arguments and the current global state — never on where it happens to be called from. This is what makes select-and-execute reliable, where you run snippets in any order.

Three rules cover everything.

Named functions see globals, not their callers

Section titled “Named functions see globals, not their callers”

A fn body resolves any free identifier — a name that is not one of its own parameters or locals — against the global scope (the top level of your script), regardless of who called it. A caller’s local variables can never leak into the functions it calls.

let tempo = 120;
fn beat_ms() {
return 60000 / tempo; // `tempo` resolves to the global
}
fn play(tempo) { // this parameter is local to `play`
return beat_ms(); // `beat_ms` still uses the GLOBAL tempo, not this one
}
print(beat_ms()); // 500
print(play(999)); // 500 (not 60000 / 999)

This keeps each function reasoning-local: to understand beat_ms, you only read its body and the globals it names.

Because free identifiers resolve against the global scope at call time, redefining a global updates every function that reads it — you do not re-define the functions.

let tempo = 120;
fn beat_ms() { return 60000 / tempo; }
print(beat_ms()); // 500
tempo = 140;
print(beat_ms()); // 428.57... (picks up the new value)

This is the property live coding relies on: tweak a global, re-execute it, and your instrument functions follow along. Definition order does not matter either — a function may reference a global defined later in the file, as long as that global exists by the time the function is called.

When you need a function to remember a local value — inside another function, or a loop — use a closure (an anonymous fn). It snapshots the variables it references at creation time (see Closures above), the deliberate opposite of a named function’s live global lookup.

fn make_adder(n) {
return fn(x) { return x + n; }; // captures `n` by value, now
}
let add5 = make_adder(5);
print(add5(3)); // 8

Functions are first-class values — they can be passed as arguments and returned from other functions.

fn apply_twice(func, x) {
return func(func(x));
}
let double = fn(x) { return x * 2; };
print(apply_twice(double, 3)); // 12

Arrays provide .map(), .filter(), and .reduce() for functional-style processing. See Arrays for details.

let data = #[1, 2, 3, 4, 5];
data.map(fn(x) { return x * 2; });
// [2, 4, 6, 8, 10]
data.filter(fn(x) { return x % 2 == 0; });
// [2, 4]
data.reduce(fn(acc, x) { return acc + x; }, 0);
// 15

Functions can call themselves.

fn factorial(n) {
if n <= 1 { return 1; }
return n * factorial(n - 1);
}
print(factorial(5)); // 120
fn fibonacci(n) {
if n <= 1 { return n; }
return fibonacci(n - 1) + fibonacci(n - 2);
}
print(fibonacci(10)); // 55

Use ...name as the last parameter to collect any remaining arguments into an array.

fn log(level, ...messages) {
print(level);
for msg in messages {
print(" " + msg);
}
}
log("INFO", "server started", "port 8080");
// INFO
// server started
// port 8080
log("WARN");
// WARN (messages is an empty array)

Use ...array in a function call to unpack an array into individual arguments.

fn add3(a, b, c) {
return a + b + c;
}
let nums = #[10, 20, 30];
print(add3(...nums)); // 60

Spread can be mixed with regular arguments:

let pair = #[2, 3];
print(add3(1, ...pair)); // 6

Rest parameters and spread arguments pair naturally:

fn sum(...nums) {
return nums.reduce(fn(acc, x) { return acc + x; }, 0);
}
let values = #[1, 2, 3, 4, 5];
print(sum(...values)); // 15

Containers (arrays, dicts, class instances) are shared when passed to functions. The function operates on the same data as the caller.

fn append_note(arr, note) {
arr.push(note);
}
let melody = #[C4, D4, E4];
append_note(melody, F4);
print(melody); // [C4, D4, E4, F4]

To avoid modifying the original, pass a .clone():

append_note(melody.clone(), G4);
print(melody); // [C4, D4, E4, F4] — unchanged

Parameters and return values can be annotated with types.

fn add(a: Number, b: Number) -> Number {
return a + b;
}
fn greet(name: String) -> String {
return "Hello, " + name;
}

Use | to allow multiple types:

fn accept(val: Number | String) -> Number | String {
return val;
}
fn to_number(input: Number | String | Boolean) -> Number {
return input + 0;
}

Higher-order functions can annotate their function parameters:

fn apply(f: (Number) -> Number, x: Number) -> Number {
return f(x);
}
print(apply(fn(n) { return n * 2; }, 5)); // 10

Use /// to attach documentation to functions and variables.

/// Clamp a value to the given range.
/// Parameters:
/// value - the value to clamp
/// lo - lower bound
/// hi - upper bound
/// Returns: the clamped value
fn clamp(value: Number, lo: Number, hi: Number) -> Number {
if value < lo { return lo; }
if value > hi { return hi; }
return value;
}

Multiple consecutive /// lines are joined together. show() displays the full typed signature alongside the doc comment, grouping it into description, parameters, and returns:

show(clamp);
// Function: clamp(value: Number, lo: Number, hi: Number) -> Number
//
// Clamp a value to the given range.
//
// Parameters:
// value: Number — the value to clamp
// lo: Number — lower bound
// hi: Number — upper bound
//
// Returns: the clamped value

Declared parameter and return types are shown when present; functions written without annotations simply display their parameter names.

Mark functions and variables as private to prevent them from being exported by a module.

private fn helper(x) {
return x * 2;
}
fn public_api(x) {
return helper(x) + 1;
}

When this file is loaded via use, only public_api is accessible — helper is hidden.

use "mylib";
mylib.public_api(5); // 11
mylib.helper(5); // Error: 'helper' is private

private also works with let:

private let INTERNAL_CONSTANT = 42;

Parameters can have default values using = expr. When a caller omits those arguments, the defaults are used instead.

fn greet(name, greeting = "Hello") {
return greeting + ", " + name + "!";
}
print(greet("Alice", "Hi")); // Hi, Alice!
print(greet("Bob")); // Hello, Bob!

Defaults are evaluated at call time in the function scope, so they can reference earlier parameters:

fn range_step(start, end, step = 1) {
let result = Array();
let i = start;
loop {
if i >= end { break; }
result.push(i);
i = i + step;
}
return result;
}
print(range_step(0, 10, 2)); // [0, 2, 4, 6, 8]
print(range_step(0, 5)); // [0, 1, 2, 3, 4]

Defaults work with closures too:

let amplify = fn(x, factor = 2) { return x * factor; };
print(amplify(5)); // 10
print(amplify(5, 3)); // 15

Pass arguments by name at the call site using name: value syntax. This lets you skip optional parameters and provide arguments in any order.

fn point(x = 0, y = 0, z = 0) {
print(f"({x}, {y}, {z})");
}
point(1, 2, 3); // (1, 2, 3)
point(z: 5); // (0, 0, 5)
point(y: 2, x: 1); // (1, 2, 0)

Named arguments can be mixed with positional arguments — positional arguments must come first:

fn create_track(name, channel, velocity = 100, port = NUL) {
print(f"{name} ch={channel} vel={velocity} port={port}");
}
create_track("drums", 10); // positional only
create_track("synth", 2, port: "USB"); // skip velocity, set port
create_track("bass", 1, velocity: 80); // skip port, set velocity

Functions are Resonon’s smallest unit of reuse — next, bundle state and behavior together into objects.