← Back to the index

4. Functions

In this chapter

Defining Functions

Functions in vv are first-class citizens. You can define named functions using the fun name(...) syntax. The function body is closed with the end keyword.

fun add(a, b)
    return a + b
end

You can also define anonymous functions (function literals) and assign them to variables:

let multiply = fun(a, b)
    return a * b
end

Recursive Functions

To define a function that calls itself, use the fun rec syntax with a named function. Note that anonymous functions (function literals) cannot be recursive.

fun rec factorial(n)
    if n <= 1
        return 1
    end
    return n * factorial(n - 1)
end

Mutually Recursive Functions

Multiple recursive functions that depend on each other can be defined simultaneously using the also keyword.

fun rec is_even(n)
    if n == 0
        return true
    end
    return is_odd(n - 1)
also is_odd(n)
    if n == 0
        return false
    end
    return is_even(n - 1)
end

Tail Call Optimization (TCO)

vv supports Tail Call Optimization for recursive functions. If the recursive call is the last action in the function (a tail call), the interpreter will optimize it to avoid consuming additional call stack frames, preventing stack overflow errors on deep recursion.

The following example demonstrates TCO. Notice that the not() operation happens before the recursive call, ensuring the recursive call itself remains in the tail position. The recursive helper can also be defined inside the outer function:

fun is_even(n)
    fun rec is_even_tail(n, is_even_acc)
        if n == 0
            return is_even_acc
        end
        // The recursive call is the very last operation, so TCO applies.
        return is_even_tail(n - 1, not(is_even_acc))
    end

    return is_even_tail(n, true)
end

In contrast, the following example does not benefit from TCO because the recursive call is not the last operation. The multiplication happens after the recursive call returns:

fun rec factorial(n)
    if n <= 1
        return 1
    end
    // TCO does NOT apply here because of the `n * ...` operation.
    return n * factorial(n - 1)
end

To make the factorial function benefit from TCO, you can use an accumulator variable to keep the recursive call in the tail position:

fun factorial(n)
    fun rec factorial_tail(n, acc)
        if n <= 1
            return acc
        end
        return factorial_tail(n - 1, n * acc)
    end

    return factorial_tail(n, 1)
end

Function Calls

Functions are called using parentheses, passing arguments inside.

let result = add(5, 3) // 8

Returning Values

The return statement immediately exits the function and evaluates the expression as the function's return value.

If a function finishes execution without reaching a return statement, it effectively returns a none record ({ type = "none" }).

Ignored Return Values

Calling a function as a standalone statement silently discards its return value. This is useful when you want to call a function only for its side effects:

import result from "std/result.vv"

fun f()
    return result.ok(123)
end

f() // OK: return value is silently discarded

You can also use the try keyword on a function call in a statement. This will propagate any error result to the caller, while silently discarding the ok value:

import file from "std/fs/file.vv"

let fd = try file.create("out.txt")
try file.write(fd, "hello") // propagates write error; ok value (bytes written) is discarded