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:
| |
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:
| |
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:
| |
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:
| |
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:
| |
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.
| |
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 withreturnor a single expression. - Scope rules — Local bindings live only inside their function; module-global
let(mutable) anddef(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) rettypes 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. voidreturn type — Functions that return nothing declarevoid, the same type used bymain.
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.
Comments
Loading comments...
Leave a Comment