Functions

Functions must be declared at the top level. A function sees its own parameters and locals, constants, and shared variables of the module, but not the program body's variables:

func multiply(int left, int right) int do
    return left * right
end

The return type is optional and defaults to void:

func greet() do
    print("hello")
    return
end

Calls to functions defined later in the file work because the generated C contains forward prototypes. Arguments must match the parameters' types, and each parameter without a default needs one. A void function may use a bare return; any other function must return a value of its type on every path. The compiler checks this simply: the function's last statement must be a return, an if with an else whose branches all end that way, or a match that handles every value and whose branches all end that way.

func sign(int x) int do
    if x < 0 then
        return -1
    else if x == 0 then
        return 0
    else
        return 1
    end
end

Defaults and named arguments

A parameter can have a default, after its name. A call may then leave it out, and any argument can be given by its parameter's name, after those given by position:

func connect(str host, int port = 80, bool tls = false) str do
    return f"{host}:{port} tls={tls}"
end

connect("example.com")                 // example.com:80 tls=false
connect("example.com", 8080)
connect("example.com", tls: true)
connect(port: 443, host: "example.com", tls: true)

Once a parameter has a default, those after it need one too. A default is evaluated at each call that leaves it out, after the values the call gives, so each call gets its own array or map; like a field's default, it may use constants and call functions, but not variables or other parameters. Methods take defaults and named arguments the same way. A function value is called with all its arguments, by position.

Overloading and nested function declarations are not supported; a function written as an expression, below, covers most uses of nested functions.

Functions as values

A function is a value like any other. Its type lists the types of its parameters and its result, as in func(int, int) bool, or func(str) for one that returns nothing. func() int[] returns an array; an array of functions puts the function type in parentheses, as in (func() int)[] steps. A named function can be passed by its name, and a function can be written as an expression with => before its result:

func twice(int n) int do
    return n * 2
end

func apply(func(int) int f, int value) int do
    return f(value)
end

print(apply(twice, 5))                    // 10
print(apply(func(int n) => n * n, 5))     // 25

Function values can be stored in variables, arrays, maps, and struct fields, returned from functions, and called with parentheses, as in handlers["save"](file). A function written as an expression may use the variables around it; their values are copied when the function is made, so a later change to the variable does not reach it:

func adder(int amount) func(int) int do
    return func(int n) => n + amount
end

func(int) int add_ten = adder(10)
print(add_ten(5))                         // 15

The parameter types can be left out where the function goes somewhere that says them, as in sort_by(words, func(a, b) => len(a) < len(b)). The result type can be written before =>: func(int n) f64 => n / 2.0. Function values cannot be compared or printed, and are never null.

A function written as an expression can also hold statements, between do and end, and return its result with return:

str[] labels = map(scores, func(int score) do
    if score >= 90 then
        return "excellent"
    end
    return "score " + to_str(score)
end)

Its result type is the one written before do, or else the one the place it goes expects, as sort_by expects a bool, or else the type of its first return with a value; without any, it returns nothing. Every path must then return a value of that type. ? works in it when its result type is a Result or an Option, written or expected. It sees the variables around it as a function written with => does; break and continue there belong to its own loops. An error inside it leaves out the whole statement that holds it when more errors are reported.

Shared variables

A function written as an expression uses the variables around it as they are when it runs, and can change them, as closures do in most languages:

int clicks = 0
func() on_click = func() do
    clicks += 1
end
on_click()
on_click()
print(clicks)                             // 2

func counter() func() int do
    int n = 0
    return func() do
        n += 1
        return n
    end
end

A variable that such a function uses, and that is assigned anywhere, in the function or around it, lives in a cell of its own, which the code around it and every function that uses it hold, so they all see the one variable. Each time its declaration runs it makes a new cell, so each call of counter gives a counter of its own. A function that calls itself can be the first value of its own variable:

func(int) int fact = func(int k) int do
    if k <= 1 then
        return 1
    end
    return k * fact(k - 1)
end

The other variables a function uses, which nothing changes, are copied into it, which is the same. Parameters and loop variables are copies too, and a function made there cannot assign to them: copy one into a variable to change it from there. shared before a variable in a function, as earlier versions needed, still works, and cuarzo lint says it can go.

Generic functions

A function can take type parameters in brackets after its name, and then works for any types:

func first[T](T[] items, T fallback) T do
    if len(items) == 0 then
        return fallback
    end
    return items[0]
end

print(first([3, 1, 2], 0), first(["a"], "none"))   // 3 a

The type arguments come from the arguments, from left to right, so a function value passed after an array can take its parameter types from the array. The compiler generates the function once for each set of types it is used with, and checks it for those types: sort on an array of structs is an error, since structs cannot be compared with <, and the error says where the function was used. A generic function cannot itself be passed as a value; wrap it, as in func(x) => first(x, 0).

A generic function can call a function of a type parameter, written as for a type, as in T.from_json(value) or T.default(): each use calls the function of its type argument, and a type argument without it is an error at that use. So code that is the same for many types, as decoding the arguments of a command with derived JSON code, is written once:

func call_json[A, R](func(A) R f, Json input) Result[Json] do
    A value = A.from_json(input)?
    return Result.OK(f(value).to_json())
end

Working with arrays through functions

Every program has these generic functions:

Function Result
map(items, f) a new array with f of each element
filter(items, keep) a new array with the elements for which keep is true
reduce(items, start, combine) start combined with each element in turn
any(items, test), all(items, test) whether test holds for some or for every element
count(items, test) how many elements test holds for
sort(items) sorts numbers, characters, or strings in place, smallest first
sort_by(items, less) sorts in place so each element comes before those it is less than
int[] numbers = [5, 3, 8, 1]
print(join(map(numbers, func(n) => to_str(n * 2)), " "))   // 10 6 16 2
print(reduce(numbers, 0, func(sum, n) => sum + n))         // 17
sort_by(people, func(a, b) => a.age < b.age)

sort and sort_by are stable merge sorts: equal elements keep their order. Their names, like those of the other functions here, cannot be declared again.