Functions
Declaring a function
fn add(a: Nat, b: Nat) -> Nat {
a + b
}
Parameter types and the return type are always written out; Albite does not infer signatures. Inside the body, inference does most of the work.
The body’s last expression is the return value, with no keyword and no trailing semicolon.
Visibility
Declarations are private to their module unless marked pub:
pub fn my_api(x: Nat) -> Nat {
x + 1
}
Early return
Since 1.5 —
returndid not exist in v1.0, where the last expression was the only way out of a function.
return leaves a function before its end, which is what you want for guard
clauses:
fn first_nonzero(a: Nat, b: Nat) -> Nat {
if a != 0 { return a; }
b
}
return has the bottom type: an expression that never produces a value, and so
fits wherever one is expected. That has one consequence worth knowing, because
the error it produces is otherwise puzzling.
An if without an else, used as a statement, must have type Unit. Writing
{ return a; } — with the semicolon — gives the block type Unit and
type-checks. Writing { return a } — without — gives the block the bottom type
and fails with a mismatch against Unit:
fn ok(a: Nat, b: Nat) -> Nat {
if a != 0 { return a; } // semicolon: block is Unit
b
}
fn broken(a: Nat, b: Nat) -> Nat {
if a != 0 { return a } // no semicolon: type error
b
}
In an if/else used as an expression, both arms can return and no semicolon
is needed, because the arms are the expression’s value:
fn pick(a: Nat, b: Nat) -> Nat {
if a != 0 { return a } else { return b }
}
The compiler also warns (W0002) about code sequenced after a return, since
it can never run.
A known divergence.
returnin statement position is currently ignored by the interpreter —albite runevaluates the unreachable code and returns that instead. Compiled binaries are correct. Until this is fixed, verifyreturn-heavy code withalbite compilerather thanalbite run. See Current limitations.
Recursion
Since 2.0-alpha — recursion is checked.
A function may call itself, but the compiler must be able to see that it stops:
some parameter has to get strictly smaller — a piece taken out of it by match
— at every recursive call. count(n - 1) on a Nat does not qualify, and is
refused with E0062. Recursion the compiler can see
ends shows what to
write instead, including #[partial], which is the ordinary answer for code
that genuinely recurses on a number.
Generic functions
Type parameters go in angle brackets:
fn identity<T>(x: T) -> T {
x
}
fn const_fn<A, B>(x: A, y: B) -> A {
x
}
Type arguments are inferred at the call site — identity(42) infers T as
Nat. See Generics for what to do when
inference cannot get there.
Lambdas
Anonymous functions bind their parameters between bars:
let inc = |x: Nat| x + 1;
let add = |a: Nat, b: Nat| a + b;
There is a second, longer spelling used mainly with the proof eliminators, where
an explicit fn reads more clearly:
let step = fn(k: Nat, acc: Nat) => acc;
Both produce ordinary function values; use whichever reads better.
Function types
Functions are first-class. A function type is written with its argument types in parentheses:
fn apply(f: (Nat) -> Nat, x: Nat) -> Nat {
f(x)
}
For a single argument the parentheses may be dropped, which is common in generic code:
fn apply_generic<A, B>(f: A -> B, x: A) -> B {
f(x)
}
Theorems
theorem is syntactically identical to fn. It declares that the signature is
a proposition and the body its proof:
theorem zero_eq_zero() -> Eq<Nat, 0, 0> {
refl
}
See Proofs.
Entry point
An executable needs a main. Its result is printed, and the process exits 0:
fn main() -> String {
"hello world"
}
Any type with a printable representation may be returned — Nat, String,
Bool, and so on.