Functions
Named functions, hoisting, overloading by arity, variadic functions, anonymous functions, arrow syntax, closures, and dispatchers.
Named Functions
Functions are declared with the func keyword. They can accept any number of parameters and return a value with return.
func add(a, b) { return a + b } add(10, 20) // 30
If a function reaches the end of its body without a return, it implicitly returns null.
func greet(name) { print("Hello, " + name) // no return → returns null }
Hoisting
Named functions declared with func are hoisted — you can call them before their definition appears in the source code. The compiler lifts function declarations to the top of their enclosing scope.
// Call BEFORE definition — this works! var result = add(10, 20) // 30 func add(a, b) { return a + b } // Call AFTER definition also works add(5, 15) // 20
func are hoisted. Anonymous functions and arrow functions assigned to variables are not hoisted.Hoisting works within any scope — including inside blocks, loops, and other functions:
var sum = 0 for (var i = 0; i < 3; i = i + 1) { sum = sum + helper() // called before definition func helper() { return 10 } } // sum is 30
Overloading by Arity
Zym supports function overloading based on the number of parameters (arity). Multiple functions with the same name but different arities can coexist. The correct version is dispatched at call time.
func process() { return "No arguments" } func process(x) { return x * 2 } func process(x, y) { return x + y } func process(x, y, z) { return x + y + z } process() // "No arguments" process(5) // 10 process(3, 7) // 10 process(1, 2, 3) // 6
Hoisting with overloading
Overloaded functions are all hoisted independently. You can call any arity variant before any of them are defined.
// Call before definition compute() // 0 compute(10) // 100 compute(5, 3) // 15 func compute() { return 0 } func compute(x) { return x * x } func compute(x, y) { return x * y }
Variadic Functions (Rest Parameters)
Functions can accept a variable number of arguments using rest parameters with the ... prefix.
The rest parameter collects all extra arguments into a regular Zym list.
func log(...args) { // args is a list: [val1, val2, val3, ...] print("got %n items", length(args)) } log(1) // args = [1] log(1, 2, 3) // args = [1, 2, 3] log() // args = []
Fixed parameters + rest
You can have fixed parameters before the rest parameter. Fixed parameters are mandatory: calling with fewer arguments than the fixed count is a runtime error.
func format(template, ...values) { // template is required, values collects the rest print(template) for (var i = 0; i < length(values); i = i + 1) { print(" arg %n: %v", i, values[i]) } } format("hello") // template="hello", values=[] format("hello", 1, 2) // template="hello", values=[1, 2] // format() // error: expected at least 1 argument
Any number of fixed parameters may precede the rest parameter. They bind positionally; everything left over lands in the rest list.
func tally(a, b, ...rest) { return a + b + length(rest) } tally(10, 20) // 30 — rest is [] tally(10, 20, 1) // 31 — rest is [1] tally(10, 20, 1, 2, 3) // 33 — rest is [1, 2, 3]
...) must always be the last parameter. Only one rest parameter is allowed per function.Variadic overload fallback
Variadic functions integrate with overloading. When both exact-arity overloads and a variadic function share the same name, exact arity matches are tried first. The variadic version acts as a fallback for any arity not covered by an exact overload.
func sum(a) { return a } func sum(a, b) { return a + b } // Variadic fallback — catches any arity not covered above func sum(...args) { var total = 0 for (var i = 0; i < length(args); i = i + 1) { total = total + args[i] } return total } sum(5) // → 5 (exact match: sum/1) sum(3, 7) // → 10 (exact match: sum/2) sum(1, 2, 3) // → 6 (no exact match → variadic fallback) sum() // → 0 (no exact match → variadic fallback)
A call to an overloaded name resolves in this order:
| Step | Condition | Result |
|---|---|---|
| 1 | Some overload declares exactly as many parameters as the call passes arguments | That overload runs |
| 2 | No exact match, a variadic overload exists, and the call passes at least as many arguments as the variadic has fixed parameters | The variadic overload runs; the surplus arguments become its rest list |
| 3 | Neither | Runtime error |
Dispatch priority
Exact arity wins however deep the ladder of overloads goes. The variadic body is reached only for argument counts no fixed-arity overload claims.
func priority() { return "zero" } func priority(a) { return "one" } func priority(a, b) { return "two" } func priority(a, b, c) { return "three" } func priority(...args) { return "variadic:" + str(length(args)) } priority() // "zero" priority(1) // "one" priority(1, 2) // "two" priority(1, 2, 3) // "three" priority(1, 2, 3, 4) // "variadic:4" priority(1, 2, 3, 4, 5) // "variadic:5"
A variadic overload that declares fixed parameters only catches calls passing at least that many arguments. Its fixed parameters bind first, and the remainder becomes the rest list. An exact-arity overload still takes precedence even when the variadic could have accepted the call with an empty rest list.
func mixed(a) { return a * 10 } func mixed(a, ...rest) { var total = a for (var i = 0; i < length(rest); i = i + 1) { total = total + rest[i] } return total } mixed(5) // 50 — mixed/1 wins, not the variadic mixed(5, 10) // 15 — fallback: a = 5, rest = [10] mixed(5, 10, 20) // 35 — fallback: a = 5, rest = [10, 20]
Function expressions and arrows
Rest parameters are not restricted to named declarations. Function expressions and both arrow forms take them, with or without preceding fixed parameters.
// Function expression var gather = func (...things) { return things } gather(10, 20, 30) // [10, 20, 30] // Function expression, fixed + rest var fexpr = func (a, b, ...rest) { return rest } fexpr(1, 2, 3, 4, 5) // [3, 4, 5] // Arrow with a block body var arrowBlock = (...items) => { return items } arrowBlock("a", "b", "c") // ["a", "b", "c"] // Arrow with fixed + rest var arrowMixed = (a, b, ...rest) => { return rest } arrowMixed(1, 2, 3, 4, 5) // [3, 4, 5] arrowMixed(1, 2) // [] // Arrow with direct return — the rest list is the expression var arrowDirect = (...args) => args arrowDirect(1, 2, 3) // [1, 2, 3]
Hoisted variadic functions
Variadic declarations are hoisted like any other named function, and so is the fallback relationship: a call written above the definitions still resolves against the complete overload set.
// All of these run before the definitions below hoistedVariadic(1, 2, 3) // [1, 2, 3] hoistedFixedRest(10, 20, 30) // [20, 30] hoistedMix(5) // 50 (exact match) hoistedMix(1, 2, 3) // 6 (variadic fallback) func hoistedVariadic(...args) { return args } func hoistedFixedRest(a, ...rest) { return rest } func hoistedMix(a) { return a * 10 } func hoistedMix(...args) { var t = 0 for (var i = 0; i < length(args); i = i + 1) { t = t + args[i] } return t }
Recursion and tail calls
A variadic function can call itself. Every call collects its own rest list from the arguments it was handed, so the length of args changes from level to level.
func recursiveSum(...args) { if (length(args) == 0) return 0 if (length(args) == 1) return args[0] return args[0] + recursiveSum(args[1]) } recursiveSum() // 0 recursiveSum(5) // 5 recursiveSum(3, 7) // 10 — the inner call passes one argument, so args is [7]
Variadic functions take part in tail-call optimization on the same terms as fixed-arity ones. The rest list is rebuilt inside the reused frame, so a tail call may hand over a different number of arguments at every hop.
func vVary(...args) { if (length(args) == 1) return args[0] if (length(args) == 3) { @tco aggressive return vVary(args[0] + args[1], args[2]) } if (length(args) == 2) { @tco aggressive return vVary(args[0] + args[1]) } return -1 } vVary(10) // 10 vVary(10, 20) // 30 vVary(10, 20, 30) // 60 — 3 args → 2 args → 1 arg
Rest parameters also compose with closures and dispatchers. See Variadic Closures & Dispatchers.
Anonymous Functions
Functions without a name can be assigned to variables or passed as arguments. They use the same func keyword but without a name.
var double = func (x) { return x * 2 } double(5) // 10
Anonymous functions are not hoisted. They must be assigned before they can be called.
As function arguments
func apply(fn, value) { return fn(value) } apply(func (x) { return x + 1 }, 10) // 11
Arrow Functions
Arrow functions are a concise syntax for anonymous functions using =>.
// Expression body (implicit return) var double = (x) => x * 2 double(5) // 10 // Block body (explicit return) var process = (x, y) => { var sum = x + y return sum * 2 } // No parameters var getPi = () => 3.14159 // Single parameter (parentheses optional) var square = x => x * x
=> expr, the expression is implicitly returned. With => { ... }, you must use an explicit return.Closures
Functions capture variables from their enclosing scope. These captured variables (upvalues) remain accessible even after the outer function returns.
func makeCounter() { var count = 0 return func () { count = count + 1 return count } } var counter = makeCounter() counter() // 1 counter() // 2 counter() // 3
Independent closure state
Each call to the outer function creates a new set of captured variables. Multiple closures from the same factory are fully independent.
var c1 = makeCounter() var c2 = makeCounter() c1() // 1 c1() // 2 c2() // 1 (independent state) c1() // 3
Shared closure state
Multiple closures returned from the same call share the same captured variables.
func makeBox(initial) { var value = initial var getter = func () { return value } var setter = func (v) { value = v } return [getter, setter] } var box = makeBox(10) var get = box[0] var set = box[1] get() // 10 set(42) get() // 42 — both closures share 'value'
Closures capturing loop variables
Be aware that closures capture the variable itself, not a snapshot of its value at creation time.
var funcs = [] for (var i = 0; i < 3; i = i + 1) { var captured = i // new variable each iteration push(funcs, func () { return captured }) } funcs[0]() // 0 funcs[1]() // 1 funcs[2]() // 2
Dispatchers
When you return an overloaded function from another function, Zym bundles all the arity variants into a single dispatcher object. The dispatcher automatically resolves to the correct overload at the call site based on the number of arguments.
func makeAdder() { func add(x) { return x + 10 } func add(x, y) { return x + y } return add // returns a dispatcher with both overloads } var adder = makeAdder() adder(5) // 15 (calls 1-arg version) adder(3, 7) // 10 (calls 2-arg version)
Dispatchers with closures
Overloaded functions inside a factory can capture shared state, and the returned dispatcher preserves all closures.
func makeAccumulator() { var total = 0 func acc() { return total } func acc(x) { total = total + x return total } return acc } var acc = makeAccumulator() acc(10) // 10 acc(20) // 30 acc() // 30 (read without adding)
Higher-order dispatchers
Dispatchers can be passed as arguments to other functions, stored in collections, or used anywhere a function value is expected.
func makeProcessor() { func proc(x) { return x * 2 } func proc(x, y) { return x + y } return proc } var p = makeProcessor() // Store in a list var ops = [p] ops[0](5) // 10 ops[0](3, 7) // 10 // Pass to another function func apply(fn, val) { return fn(val) } apply(p, 5) // 10
Multiple dispatchers
A factory can create and return multiple independent dispatchers.
func makeOps() { func math(x) { return x * x } func math(x, y) { return x + y } func text(s) { return "[" + s + "]" } func text(a, b) { return a + " " + b } return [math, text] } var ops = makeOps() ops[0](5) // 25 (math/1) ops[0](3, 4) // 7 (math/2) ops[1]("hi") // "[hi]" (text/1) ops[1]("hello", "world") // "hello world" (text/2)
Variadic Closures & Dispatchers
A variadic function is an ordinary closure: it captures upvalues, outlives the call that created it, and can be returned, stored, or passed on. It also participates in dispatchers, where it serves as the fallback arm.
func wrapInVariadic() { func inner(...args) { return args } return inner } var wrapped = wrapInVariadic() wrapped(1, 2, 3) // [1, 2, 3]
Capturing outer variables
The rest list belongs to the individual call; the captured variables belong to the enclosing frame. A variadic closure reads its factory’s parameters and locals exactly like a fixed-arity one.
func makeCollector(prefix) { func collect(...items) { return prefix + ":" + str(length(items)) } return collect } var collector = makeCollector("test") collector() // "test:0" collector(1) // "test:1" collector(1, 2, 3) // "test:3"
Mutating captured state
A variadic closure writes to its upvalues like any other closure. The state persists between calls no matter how many arguments each call supplies.
func makeSummer() { var total = 0 func addAll(...nums) { for (var i = 0; i < length(nums); i = i + 1) { total = total + nums[i] } return total } return addAll } var acc = makeSummer() acc(1, 2, 3) // 6 acc(4, 5) // 15 acc(10) // 25 acc() // 25 — no arguments, no change
Nested closures
A variadic at the bottom of a nesting chain sees every enclosing scope.
func outerNested(x) { func middleNested(y) { func innerVariadic(...args) { var sum = x + y for (var i = 0; i < length(args); i = i + 1) { sum = sum + args[i] } return sum } return innerVariadic } return middleNested } var n1 = outerNested(10) var n2 = n1(20) n2() // 30 (x + y) n2(5) // 35 n2(1, 2, 3) // 36
Anonymous and arrow variadic closures
Function expressions and arrows capture the same way, so a factory can return either form.
func makeArrowVar(offset) { return (...args) => { var total = offset for (var i = 0; i < length(args); i = i + 1) { total = total + args[i] } return total } } var av1 = makeArrowVar(100) av1() // 100 av1(1, 2, 3) // 106
Each call to the factory yields an independent closure, exactly as for fixed-arity closures; the func (...args) { ... } expression form behaves identically.
Shared upvalues across sibling closures
Closures created by the same call share their captured variables even when only one of them is variadic. The rest list is private to each call, but the upvalue belongs to the enclosing frame, so a fixed-arity sibling observes every write the variadic makes.
var varGetter var varAdder func setupVarShared() { var total = 0 varGetter = func () { return total } varAdder = func (...nums) { for (var i = 0; i < length(nums); i = i + 1) { total = total + nums[i] } return total } } setupVarShared() varGetter() // 0 varAdder(1, 2, 3) // 6 varGetter() // 6 — the getter sees the variadic's writes varAdder(4) // 10 varGetter() // 10
Dispatchers with a variadic overload
A factory that declares several arities of one name plus a variadic one returns a dispatcher carrying the variadic as its fallback. Call-site resolution follows the same order as for top-level overloads.
func makeVariadicDispatcher() { func handler(x) { return x * 2 } func handler(x, y) { return x + y } func handler(...args) { return length(args) * 100 } return handler } var disp = makeVariadicDispatcher() disp(5) // 10 (handler/1) disp(3, 7) // 10 (handler/2) disp() // 0 (fallback, args = []) disp(1, 2, 3) // 300 (fallback) disp(1, 2, 3, 4) // 400 (fallback)
Dispatchers over shared state
Every arm of a dispatcher, including the variadic one, closes over the same variables, so state written through one arity is visible through the others.
func makeStatefulDispatcher(base) { var count = 0 func op() { count = count + 1 return base + count } func op(x) { count = count + x return base + count } func op(...args) { for (var i = 0; i < length(args); i = i + 1) { count = count + args[i] } return base + count } return op } var sd = makeStatefulDispatcher(100) sd() // 101 (op/0) sd(5) // 106 (op/1) sd(1, 2, 3) // 112 (fallback) sd() // 113 — state persists across arities
Separate calls to the factory carry separate state: makeStatefulDispatcher(0) and makeStatefulDispatcher(1000) each own their count.
Variadic dispatchers as values
A dispatcher holding a variadic fallback is a value like any other. Storing it in a collection, passing it to a function, or copying it to another variable preserves the whole overload set.
func makeVarDisp() { func fn(x) { return x } func fn(...args) { return length(args) * 100 } return fn } // Stored in a list var dispList = [makeVarDisp(), makeVarDisp()] dispList[0](42) // 42 (exact) dispList[0](1, 2, 3) // 300 (fallback) dispList[1](7) // 7 // Passed as an argument func useDispatcher(f) { var r1 = f(10) var r2 = f(1, 2, 3) return r1 + r2 } useDispatcher(makeVarDisp()) // 310 (10 + 300) // Copied between variables var d = makeVarDisp() var e = d e(1, 2) // 200 — the copy dispatches identically e(7) // 7
f inside useDispatcher is an ordinary parameter. The overload is resolved on every call, so a single parameter reaches both the fixed-arity and the variadic body.Functions as Values
Functions are first-class values. They can be stored in variables, lists, maps, and struct fields, passed as arguments, and returned from other functions.
// Store in a map var ops = { add: func (a, b) { return a + b }, mul: func (a, b) { return a * b } } ops.add(3, 4) // 7 ops.mul(3, 4) // 12 // Store in a list var transforms = [ (x) => x * 2, (x) => x + 1, (x) => x * x ] transforms[0](5) // 10 transforms[2](4) // 16 // Higher-order: function that returns a function func multiplier(factor) { return (x) => x * factor } var triple = multiplier(3) triple(7) // 21