Functions

Named functions, hoisting, overloading by arity, 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 }

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)

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