Chapter 2: Functions That Shine
Functions are the heart of Tova. A well-written Tova program is a collection of small, focused functions that compose together to solve big problems. This chapter covers every function form in the language — and by the end, you'll build a math toolkit that computes derivatives using nothing but functions.
Declaring Functions
The fn keyword declares a function. The last expression in the body is the return value:
fn greet(name) {
"Hello, {name}!"
}
fn add(a, b) {
a + b
}
// Single-expression functions can be compact
fn double(x) { x * 2 }
fn is_positive(x) { x > 0 }No return keyword needed for the common case. The body is an expression, and expressions produce values.
Visibility with pub
By default, functions are file-private — they can only be called within the same module. To make a function accessible from other files, prefix it with pub:
// pub makes a function accessible from other modules
pub fn create_user(name: String) -> User {
User(name: name, role: "member")
}
// Without pub, functions are file-private
fn validate_name(name: String) -> Bool {
len(name) >= 2
}This is intentional. Most functions are implementation details. Only mark functions pub when they're part of a module's public interface. If another file imports your module, it can only see the pub functions.
When to Use return
Use return only for early exits — when you want to bail out of a function before reaching the end:
fn find_first_negative(items) {
for item in items {
if item < 0 { return item }
}
None
}For everything else, let the last expression be the return value.
Default Parameters
Give parameters default values with =:
fn greet(name, greeting = "Hello") {
"{greeting}, {name}!"
}
print(greet("Alice")) // "Hello, Alice!"
print(greet("Alice", "Bonjour")) // "Bonjour, Alice!"fn create_user(name, role = "member", active = true) {
{ name: name, role: role, active: active }
}
user = create_user("Alice")
admin = create_user("Bob", "admin")Type Annotations on Functions
Annotate parameters and return types for clarity:
fn calculate_area(width: Float, height: Float) -> Float {
width * height
}
fn is_valid_email(email: String) -> Bool {
contains(email, "@")
}Type annotations are optional but excellent documentation. Use them on public-facing functions and when the types aren't obvious from the code.
Doc Comments
Use /// (triple-slash) to document your functions. Doc comments describe what a function does, what it expects, and what it returns:
/// Calculates the distance between two 2D points.
/// Returns the Euclidean distance as a Float.
fn distance(x1: Float, y1: Float, x2: Float, y2: Float) -> Float {
dx = x2 - x1
dy = y2 - y1
(dx * dx + dy * dy) ** 0.5
}Doc comments are more than just notes for readers. The Tova LSP picks up /// comments and displays them as hover documentation in your editor. When you hover over a call to distance(), you'll see the description right there.
/// Creates a new user with the given name and default role.
/// The role defaults to "member" and can be changed later.
pub fn create_user(name: String) -> User {
User(name: name, role: "member")
}
/// Checks whether a string looks like a valid email address.
/// This is a simple check — it only verifies the presence of "@".
fn is_valid_email(email: String) -> Bool {
contains(email, "@")
}Writing Good Doc Comments
Put /// on the lines immediately above the function declaration. The first line should be a concise summary. Additional lines can elaborate on parameters, edge cases, or return values. Regular // comments are ignored by the LSP — only /// is treated as documentation.
Lambdas (Anonymous Functions)
Lambdas are functions without names. They're written as fn(params) body:
double = fn(x) x * 2
add = fn(a, b) a + bLambdas are essential for working with higher-order functions:
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
evens = numbers |> filter(fn(x) x % 2 == 0)
squared = numbers |> map(fn(x) x * x)
total = numbers |> reduce(fn(acc, x) acc + x, 0)Multi-line lambdas use braces:
process = fn(items) {
items
|> filter(fn(x) x > 0)
|> map(fn(x) x * 2)
|> sorted()
}Lambda Syntax
Tova lambdas use fn(x) body, not fn(x) => body. There's no arrow. The body follows the parameter list directly.
Higher-Order Functions
A higher-order function takes a function as an argument or returns a function. You've already seen map, filter, and reduce — they're all higher-order functions from the stdlib. Let's write our own:
// Takes a function, applies it to every element
fn apply_to_all(items, f) {
var result = []
for item in items {
result.push(f(item))
}
result
}
labels = apply_to_all(["alice", "bob"], fn(name) upper(name))
print(labels) // ["ALICE", "BOB"]// Returns a function that checks a threshold
fn above(threshold) {
fn(x) x > threshold
}
numbers = [10, 25, 3, 47, 8, 31]
big_numbers = numbers |> filter(above(20))
print(big_numbers) // [25, 47, 31]Closures
When a function captures variables from its surrounding scope, it creates a closure. The captured variables live as long as the function does:
fn make_greeter(greeting) {
fn(name) "{greeting}, {name}!"
}
hello = make_greeter("Hello")
hola = make_greeter("Hola")
print(hello("World")) // "Hello, World!"
print(hola("Mundo")) // "Hola, Mundo!"Closures are powerful for creating specialized functions:
fn multiplier(factor) {
fn(x) x * factor
}
triple = multiplier(3)
percent = multiplier(0.01)
print(triple(7)) // 21
print(percent(250)) // 2.5fn make_counter(start) {
var n = start
{
next: fn() { n += 1; n },
reset: fn() { n = start },
value: fn() n
}
}
ctr = make_counter(0)
print(ctr.next()) // 1
print(ctr.next()) // 2
print(ctr.next()) // 3
ctr.reset()
print(ctr.value()) // 0Destructuring Parameters
Functions can destructure their arguments directly in the parameter list:
// Object destructuring
fn full_name({first, last}) {
"{first} {last}"
}
user = { first: "Alice", last: "Smith", age: 30 }
print(full_name(user)) // "Alice Smith"// Array destructuring
fn head_and_tail([head, ...tail]) {
print("Head: {head}")
print("Tail: {tail}")
}
head_and_tail([1, 2, 3, 4, 5])// Combine with defaults
fn connect({host = "localhost", port = 3000}) {
print("Connecting to {host}:{port}")
}
connect({ port: 8080 }) // "Connecting to localhost:8080"Variadic Functions (Rest Parameters)
A function can accept a variable number of arguments using the ... rest syntax:
fn log_all(prefix, ...messages) {
for msg in messages {
print("[{prefix}] {msg}")
}
}
log_all("INFO", "Server started", "Port 3000", "Ready")
// [INFO] Server started
// [INFO] Port 3000
// [INFO] ReadyThe rest parameter ...messages collects all extra arguments into an array. It must be the last parameter:
fn sum_all(...numbers) {
numbers |> reduce(fn(acc, n) acc + n, 0)
}
print(sum_all(1, 2, 3, 4, 5)) // 15Combine with the spread operator to forward arguments:
fn log_error(...args) {
log_all("ERROR", ...args)
}Named Arguments
When calling functions with many parameters, named arguments make the call site more readable:
fn create_server(host, port, debug, max_connections) {
print("Starting {host}:{port} (debug={debug}, max={max_connections})")
}
// Positional — hard to read, easy to mix up
create_server("localhost", 8080, false, 100)
// Named — clear what each value means
create_server(host: "localhost", port: 8080, debug: false, max_connections: 100)Named arguments can be in any order:
create_server(port: 8080, host: "0.0.0.0", max_connections: 50, debug: true)You can also mix positional and named arguments — positional arguments come first:
create_server("localhost", 8080, debug: true, max_connections: 200)When to Use Named Arguments
Use named arguments when a function takes more than 2-3 parameters, especially boolean flags or numeric values where the meaning isn't obvious from the value alone. create_server("localhost", 8080, false, 100) is cryptic; create_server(host: "localhost", port: 8080, debug: false, max_connections: 100) is self-documenting.
Decorators
Decorators modify function behavior using the @ prefix. Tova has built-in decorators and supports custom ones:
Built-in Decorators
// @wasm — compile to WebAssembly for maximum performance
@wasm fn fibonacci(n: i32) -> i32 {
if n <= 1 { return n }
fibonacci(n - 1) + fibonacci(n - 2)
}
// @fast — use TypedArrays for numerical work
@fast fn dot_product(a: [Float], b: [Float]) -> Float {
var total = 0.0
for i in range(len(a)) {
total += a[i] * b[i]
}
total
}
// @memoize — cache results for repeated calls
@memoize fn expensive_compute(n) {
// ... heavy computation ...
n * n
}Decorators with Arguments
Some decorators accept configuration:
@fast fn vector_add(a: [Float], b: [Float]) -> [Float] {
// @fast with typed parameters
typedAdd(a, b)
}Stacking Decorators
Multiple decorators can be stacked on a single function:
@memoize
@fast
fn cached_dot(a: [Float], b: [Float]) -> Float {
typedDot(a, b)
}Decorators are applied bottom-up — @fast is applied first, then @memoize wraps the result.
We cover @fast and @wasm in depth in the Performance chapter.
Recursion
Tova supports recursion naturally. Use it when a problem has recursive structure:
fn factorial(n) {
if n <= 1 { 1 }
else { n * factorial(n - 1) }
}Pattern matching makes recursive functions elegant:
fn fib(n) {
match n {
0 => 0
1 => 1
n => fib(n - 1) + fib(n - 2)
}
}Recursive data processing:
fn depth(tree) {
match tree {
{ left: l, right: r } => 1 + max_of(depth(l), depth(r))
_ => 0
}
}Recursion vs. Iteration
Use recursion when the problem is naturally recursive (trees, nested structures, divide-and-conquer). Use iteration (for, while) for flat data or when performance matters. Tova doesn't currently optimize tail calls, so very deep recursion can overflow the stack.
Generic Functions
When a function works with any type, use type parameters to express that:
fn identity<T>(x: T) -> T {
x
}
fn first_of<T>(items: [T]) -> T {
items[0]
}
fn pair<A, B>(a: A, b: B) -> (A, B) {
(a, b)
}Generic functions are especially useful for utility functions that don't care about the specific type:
fn swap<T>(items: [T], i: Int, j: Int) -> [T] {
var result = [...items]
temp = result[i]
result[i] = result[j]
result[j] = temp
result
}
fn repeat_value<T>(value: T, n: Int) -> [T] {
var result = []
for _ in range(n) {
result.push(value)
}
result
}
print(repeatValue("hello", 3)) // ["hello", "hello", "hello"]
print(repeatValue(42, 5)) // [42, 42, 42, 42, 42]When to Use Generics
Tova infers types aggressively, so you often don't need to write generic annotations — fn identity(x) { x } works fine. But generics are valuable in library code where you want to document the relationship between input and output types, or when you're building data structures that work with any element type.
Generators
Generators produce values lazily using yield. Any function that contains a yield expression is automatically treated as a generator — no special syntax is needed:
fn naturals() {
var n = 0
while true {
yield n
n += 1
}
}
fn take(iter, n_items) {
var count = 0
for item in iter {
if count >= n_items { return }
yield item
count += 1
}
}
// Get first 5 natural numbers
var first_five = []
for item in take(naturals(), 5) {
first_five.push(item)
}
print(first_five) // [0, 1, 2, 3, 4]Generators are useful for:
- Infinite sequences
- Processing large datasets without loading everything into memory
- Custom iteration patterns
Extern Declarations
When you need to call JavaScript or native functions that Tova doesn't know about, use extern to declare them:
extern fn crypto_hash(data: String) -> String
extern fn native_sort(arr: [Int]) -> [Int]extern tells the compiler "this function exists at runtime — trust me." No implementation body is needed. This is how you bridge to native libraries, WebAssembly modules, or JavaScript APIs that aren't in the stdlib.
extern fn performance_now() -> Float
fn benchmark(label, f) {
start = performance_now()
f()
elapsed = performance_now() - start
print("{label}: {elapsed}ms")
}Use Sparingly
extern bypasses Tova's type checking for the declared function. Incorrect declarations will cause runtime errors rather than compile-time errors. Prefer Tova's stdlib functions when available.
Project: Math Toolkit
Let's build a toolkit that demonstrates higher-order functions and closures working together:
// Compose: chain two functions into one
fn compose(f, g) {
fn(x) f(g(x))
}
// Apply a function n times
fn apply_n(f, n_times, start) {
var result = start
for _ in range(n_times) {
result = f(result)
}
result
}
// Numerical derivative approximation
fn derivative(f, dx) {
fn(x) (f(x + dx) - f(x)) / dx
}
// Sum a mathematical series
fn sum_series(from_n, to_n, term) {
var total = 0.0
for i in range(from_n, to_n + 1) {
total += term(i)
}
total
}
// Put it all together
square = fn(x) x * x
cube = fn(x) x * x * x
// Compose: square after increment
square_plus_one = compose(square, fn(x) x + 1)
print(square_plus_one(4)) // 25 = (4+1)²
// Derivative of x² ≈ 2x
d_square = derivative(square, 0.0001)
print(d_square(3.0)) // ≈ 6.0
// 2^10 by doubling 10 times
print(apply_n(fn(x) x * 2, 10, 1)) // 1024
// Approximate pi with Leibniz series
pi = 4.0 * sum_series(0, 10000, fn(k) {
sign = if k % 2 == 0 { 1.0 } else { -1.0 }
sign / (2.0 * toFloat(k) + 1.0)
})
print("Pi ≈ {pi}")The key insight: derivative returns a function. You pass it a function and get back a new function that computes the derivative at any point. This is the power of higher-order functions.
Exercises
Exercise 2.1: Write a memoize(f) function that takes a single-argument function and returns a new function that caches results. If called with the same argument twice, it should return the cached result instead of recomputing. Hint: use a closure over a mutable object.
Exercise 2.2: Write a pipe(...fns) function that takes any number of single-argument functions and returns a new function that applies them left to right. pipe(f, g, h)(x) should equal h(g(f(x))). Test it by piping double, add_one, and toString together.
Exercise 2.3: Write a retry(f, n_times) function that calls f() and if it returns Err, retries up to n_times. Return the first Ok result or the last Err. Hint: use a for loop with early return.
Challenge
Build a function calculator that can:
- Take a mathematical function as a string description (e.g., "square", "double", "increment")
- Look it up from a registry (an object mapping names to functions)
- Compose multiple named functions together
- Compute the numerical derivative
- Print a table of values for x = 0 to 10
For example: "double then square" should produce [0, 4, 16, 36, 64, ...].
← Previous: Thinking in Tova | Next: Mastering Collections →