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.

basic function
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.

hoisting
// 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
Note: Only named functions declared with 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:

hoisting in loops
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.

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

pure variadic
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.

fixed + rest
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]
Rule: The rest parameter (...) 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.

overload + variadic fallback
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:

StepConditionResult
1Some overload declares exactly as many parameters as the call passes argumentsThat overload runs
2No exact match, a variadic overload exists, and the call passes at least as many arguments as the variadic has fixed parametersThe variadic overload runs; the surplus arguments become its rest list
3NeitherRuntime error
Rule: At most one variadic overload per function name is allowed. A second one is treated as a redefinition of the first, regardless of how many fixed parameters it declares.

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.

priority ladder
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.

fallback with fixed params
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.

variadic forms
// 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.

hoisted variadics
// 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.

recursive variadic
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.

varying arity under TCO
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
See also: mutual tail calls between a fixed-arity function and a variadic one are optimized in both directions, fixed parameters and the rest list survive frame reuse, and a variadic closure recurses in constant stack space. See Tail-Call Optimization.

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.

anonymous functions
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 =>.

arrow functions
// 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
Expression vs block: With => 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.

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

shared state
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.

loop capture
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.

basic dispatcher
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.

dispatcher with state
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.

returning a variadic
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.

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

variadic accumulator
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.

nested capture
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.

arrow closure
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.

variadic sibling closures
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
Note: the two closures have different arities and never call each other, yet they are not independent. Closures built in the same frame share the variables they capture, and a rest parameter changes nothing about that.

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.

dispatcher with fallback
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.

stateful dispatcher
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.

dispatchers as values
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
Note: 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.

functions as values
// 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