Intermediate

Functions in Hare

Learn how to define and call functions in Hare - parameters, return values, scope, recursion, function pointers, and variadic functions with Docker-ready examples

Functions are the primary unit of code organization in Hare. As an imperative, procedural systems language, Hare keeps functions deliberately simple — there is no method dispatch, no operator overloading, and no hidden control flow. A function takes typed parameters, returns a single typed value, and that is the whole story.

Hare’s conservative design shows up clearly in its function model. Parameters and return types are always explicit, function bodies are expressions (which is why they use = and end with ;), and functions can be referenced through pointers for higher-order programming. Notably, Hare has no default parameter values — instead, variadic parameters cover the cases where other languages reach for defaults or overloading.

In this tutorial you will learn how to define and call functions, pass parameters and return values, work with local versus module-global scope, write recursive functions, pass functions as values using function pointers, and accept a variable number of arguments with variadic parameters.

Defining and Calling Functions

A Hare function is declared with fn, a name, a typed parameter list, a return type, and a body introduced with =. The body can be a { ... } block (using return) or a single expression. The export keyword is only needed when a function must be visible outside its module (like main); plain helper functions omit it.

Create a file named functions.ha:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
use fmt;

// Block body: uses an explicit return statement
fn add(a: int, b: int) int = {
	return a + b;
};

// Expression body: no braces and no return needed
fn square(n: int) int = n * n;

// A function that returns nothing uses the 'void' type
fn greet(name: str) void = {
	fmt::printfln("Hello, {}!", name)!;
};

export fn main() void = {
	const sum = add(3, 4);
	const sq = square(5);

	fmt::printfln("add(3, 4) = {}", sum)!;
	fmt::printfln("square(5) = {}", sq)!;
	greet("Hare");
};

Each parameter must declare its type (a: int), and the return type follows the parameter list. Because function bodies are expressions, even a multi-statement block ends with a semicolon after the closing brace.

Variable Scope: Local vs Global

Bindings declared inside a function are local — they exist only for the duration of that call. Bindings declared at module level are global and shared by every function in the file. Use let for a mutable binding and def for a compile-time constant.

Create a file named scope.ha:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use fmt;

// Module-global mutable binding - shared across all functions
let count: int = 0;

// Compile-time constant
def MAX: int = 100;

fn increment() void = {
	// Reads and modifies the global binding
	count += 1;
};

fn describe() void = {
	// Local binding - only visible inside this function
	const label = "current count";
	fmt::printfln("{}: {} (max {})", label, count, MAX)!;
};

export fn main() void = {
	increment();
	increment();
	increment();
	describe();
};

The global count is modified by increment and read by describe, while label is local to describe and cannot be referenced anywhere else. Hare requires explicit types on module-global let bindings, but local bindings can rely on type inference.

Recursion

Hare functions can call themselves, which makes recursion the natural way to express problems like factorials and Fibonacci numbers. Each call gets its own stack frame and its own local bindings.

Create a file named recursion.ha:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
use fmt;

fn factorial(n: u64) u64 = {
	if (n <= 1) {
		return 1;
	};
	return n * factorial(n - 1);
};

fn fib(n: u64) u64 = {
	if (n < 2) {
		return n;
	};
	return fib(n - 1) + fib(n - 2);
};

export fn main() void = {
	fmt::printfln("factorial(5) = {}", factorial(5))!;
	fmt::printfln("fib(10) = {}", fib(10))!;
};

The unsigned 64-bit type u64 is used here because factorials grow quickly. As with all control flow in Hare, the if blocks end with a semicolon after the closing brace.

Higher-Order Functions with Function Pointers

Hare supports passing functions as values through function pointers. A function pointer type is written *fn(args) ret, and the & operator takes the address of a named function. This lets you write higher-order functions that accept behavior as a parameter.

Create a file named higher_order.ha:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
use fmt;

fn double(x: int) int = x * 2;
fn negate(x: int) int = -x;

// Accepts a pointer to any function of type fn(int) int
fn apply(f: *fn(int) int, value: int) int = {
	return f(value);
};

export fn main() void = {
	// '&' takes the address of a function
	fmt::printfln("apply(double, 21) = {}", apply(&double, 21))!;
	fmt::printfln("apply(negate, 7) = {}", apply(&negate, 7))!;
};

Inside apply, the parameter f is called just like a normal function: f(value). This is the foundation for callbacks, dispatch tables, and other patterns where behavior is selected at runtime.

Variadic Functions

Since Hare has no default parameter values, variadic parameters fill that gap when a function needs to accept a flexible number of arguments. A trailing type... parameter collects the extra arguments into a slice, which you can index and measure with len.

Create a file named variadic.ha:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
use fmt;

// 'nums' collects any number of int arguments into a slice
fn sum(nums: int...) int = {
	let total = 0;
	for (let i = 0z; i < len(nums); i += 1) {
		total += nums[i];
	};
	return total;
};

export fn main() void = {
	fmt::printfln("sum() = {}", sum())!;
	fmt::printfln("sum(1, 2, 3) = {}", sum(1, 2, 3))!;
	fmt::printfln("sum(10, 20, 30, 40) = {}", sum(10, 20, 30, 40))!;
};

The literal 0z is a size-typed zero, matching the type returned by len. The variadic parameter behaves like a []int slice inside the function, so ordinary indexing and iteration work as expected.

Running with Docker

Since there is no dedicated Hare image on Docker Hub, we use Alpine Linux’s edge repository, which packages the Hare toolchain. The apk add step installs the compiler before running each program.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Pull the Alpine edge image
docker pull alpine:edge

# Run the basic functions example
docker run --rm -v $(pwd):/code alpine:edge sh -c "apk add --no-cache hare > /dev/null 2>&1 && cd /code && hare run functions.ha"

# Run the scope example
docker run --rm -v $(pwd):/code alpine:edge sh -c "apk add --no-cache hare > /dev/null 2>&1 && cd /code && hare run scope.ha"

# Run the recursion example
docker run --rm -v $(pwd):/code alpine:edge sh -c "apk add --no-cache hare > /dev/null 2>&1 && cd /code && hare run recursion.ha"

# Run the higher-order functions example
docker run --rm -v $(pwd):/code alpine:edge sh -c "apk add --no-cache hare > /dev/null 2>&1 && cd /code && hare run higher_order.ha"

# Run the variadic functions example
docker run --rm -v $(pwd):/code alpine:edge sh -c "apk add --no-cache hare > /dev/null 2>&1 && cd /code && hare run variadic.ha"

Expected Output

Running functions.ha:

add(3, 4) = 7
square(5) = 25
Hello, Hare!

Running scope.ha:

current count: 3 (max 100)

Running recursion.ha:

factorial(5) = 120
fib(10) = 55

Running higher_order.ha:

apply(double, 21) = 42
apply(negate, 7) = -7

Running variadic.ha:

sum() = 0
sum(1, 2, 3) = 6
sum(10, 20, 30, 40) = 100

Key Concepts

  • Explicit types — Every parameter declares its type and every function declares a return type; Hare never infers function signatures.
  • Expression bodies — Function bodies are expressions, which is why they use = and end with ;; a body can be a { ... } block with return or a single expression.
  • Scope rules — Local bindings live only inside their function; module-global let (mutable) and def (constant) bindings are shared across the file and require explicit types.
  • Recursion — Functions may call themselves; each call gets its own stack frame, making recursion a clean fit for factorials, Fibonacci, and tree traversals.
  • Function pointers*fn(args) ret types plus the & operator let you pass functions as values and write higher-order functions.
  • No default parameters — Hare intentionally omits default argument values; use variadic type... parameters when you need a flexible argument count.
  • void return type — Functions that return nothing declare void, the same type used by main.

Next Steps

Continue to I/O Operations to learn how Hare reads from standard input, writes to files, and handles I/O errors with tagged unions.

Running Today

All examples can be run using Docker:

docker pull alpine:edge
Last updated:

Comments

Loading comments...

Leave a Comment

2000 characters remaining