fn declares a callable unit. The same keyword introduces free functions,
methods, static functions, trait requirements, and prelude signatures.
Function and parameter names use snake_case.
fn add(a: int, b: int): int {
return a + b
}
fn main(): void {
println("${add(20, 22)}")
}Bodies
A block body holds statements, early returns, and an optional final tail
expression. The tail expression is the result — no return keyword needed.
fn absolute(value: int): int {
if value < 0 {
return -value
}
value
}
fn category(value: int): string {
if value < 0 {
return "negative"
}
if value == 0 {
return "zero"
}
"positive"
}
fn main(): void {
println("${absolute(-7)} ${category(0)} ${category(3)}")
}When the whole body is one expression, use => instead of braces.
fn double(value: int): int => value * 2
fn is_even(value: int): bool => value % 2 == 0
fn describe(value: int): string => "${value} is even: ${is_even(value)}"
fn main(): void {
println(describe(double(21)))
}An => body is checked against the declared return type exactly like a
block’s tail expression. It may be any expression, including an if or a
match:
enum Level { Low, High }
fn threshold(l: Level): int => match l {
Low => 10
High => 90
}
fn clamp_high(value: int): int => if value > 90 { 90 } else { value }
fn main(): void {
println("${threshold(High)} ${clamp_high(120)}")
}Give every normally completing path a value. A path that returns, propagates an error, or otherwise diverges does not need to produce the block’s tail value, so an early-return branch never forces the remaining branch to manufacture a placeholder.
Parameters
Parameters are name-and-type pairs, and calls supply them positionally.
fn clamp(value: int, minimum: int, maximum: int): int {
if value < minimum { return minimum }
if value > maximum { return maximum }
return value
}
fn main(): void {
println("${clamp(150, 0, 100)} ${clamp(-5, 0, 100)} ${clamp(42, 0, 100)}")
}Argument count is checked exactly:
fn add(a: int, b: int): int => a + b
fn main(): void {
println("${add(1)}")
}Named call arguments and variadic parameters are not part of the language. When an argument list grows past what a reader can keep straight, take a struct instead of adding more positional parameters.
struct ConnectOptions {
host: string
port: int
secure: bool
timeout_ms: int
}
fn connect(options: ConnectOptions): string {
scheme := if options.secure { "https" } else { "http" }
return "${scheme}://${options.host}:${options.port} (${options.timeout_ms}ms)"
}
fn main(): void {
println(connect(ConnectOptions {
host: "db.internal",
port: 6432,
secure: false,
timeout_ms: 250,
}))
}Defaults
A parameter may carry a default value. Defaults fill an omitted trailing suffix, left to right.
const DEFAULT_PORT: int = 5432
fn connect(host: string, port: int = DEFAULT_PORT, secure: bool = true): string {
scheme := if secure { "https" } else { "http" }
return "${scheme}://${host}:${port}"
}
fn main(): void {
println(connect("db.internal"))
println(connect("db.internal", 6432))
println(connect("db.internal", 6432, false))
}Because calls are positional, a caller cannot skip port and supply only
secure. Once a parameter has a default, every parameter after it must have
one too:
fn f(a: int = 1, b: int): int => a + b
fn main(): void {
println("${f(1, 2)}")
}Default expressions fold at compile time. They may use literals and constants, but they cannot read the caller’s runtime state.
Mutable parameters
Prefix a parameter with mut when the body assigns to it or mutates through
it. The final value is written back to the caller’s place.
fn bump(mut value: int): int {
value += 1
return value
}
fn append_marker(mut values: []string): void {
values.add("done")
}
fn main(): void {
mut names := ["start"]
append_marker(names)
println("${bump(41)} ${names.len()} ${names[1] ?? "?"}")
}The argument has to be a mutable place — a mut binding, or a field or
index of one. An immutable binding or a literal cannot satisfy the contract:
fn append_marker(mut values: []string): void {
values.add("done")
}
fn main(): void {
names := ["start"]
append_marker(names)
println("${names.len()}")
}Return types
Write : T after the parameter list. void is the spelling for “no useful
result”; Unit is accepted as a synonym, and the annotation may be omitted
entirely.
fn answer(): int {
return 42
}
fn log_ready(): void {
println("ready")
}
fn log_done(): Unit {
println("done")
}
fn total(xs: []int) {
mut sum := 0
for x in xs {
sum += x
}
return sum
}
fn main(): void {
log_ready()
log_done()
println("${answer()} ${total([1, 2, 3])}")
}total has no annotation, so the checker infers int from its return. That
is convenient inside a file and risky across one: an edit to the body silently
changes the API. State the type on anything other code depends on.
Record results
A function can return an anonymous record when the caller only needs a few fields together and a named struct would be ceremony. The type is structural: any record with the same field names and types is the same type, so a second declaration can accept the result without either one naming a struct.
fn metrics(xs: []int): { count: int, total: int, empty: bool } {
mut total := 0
for x in xs {
total += x
}
return { count: xs.len(), total: total, empty: xs.is_empty() }
}
fn describe(m: { count: int, total: int, empty: bool }): string {
if m.empty {
return "no samples"
}
return "${m.count} samples, total ${m.total}"
}
fn main(): void {
println(describe(metrics([3, 4, 5])))
println(describe(metrics([])))
m := metrics([1, 2])
println("${m.count} ${m.total} ${m.empty}")
}Repeat the field list once too often and a named struct has become the better trade — the record form is for a result that only one or two call sites ever destructure.
The ::result projection
fnname::result names a declaration’s success type, so a helper can follow a
signature without restating it.
fn version_code(): int => 140
fn is_supported(v: version_code::result): bool => v >= 100
fn main(): void {
println("${is_supported(version_code())}")
}Today the projection only survives lowering when the projected type is a
scalar. Naming an aggregate result — a struct, a list, or an anonymous
record — through ::result passes the type checker and then fails to build
with ATOLL5005, so write those types out until the backend catches up. Its
sibling fnname::err, covered under fallible functions,
has no such restriction.
Fallible functions
A function that can fail declares T ! E: T on success, E in the error
channel.
error ParseError { Empty, NotANumber }
fn parse_port(text: string): int ! ParseError {
if text.len() == 0 {
error Empty
}
match text.to_int() {
Some(v) => return v
None => error NotANumber
}
}
fn open(text: string): string ! ParseError {
port := parse_port(text)?
return "listening on ${port}"
}
fn main(): void {
println(open("8080").unwrap_or("bad port"))
println(open("nope").unwrap_or("bad port"))
}Inside the body, return and the tail expression produce the success type,
and error Variant exits through the error channel. At a call site the value
is a Result[T, E]: ? propagates it, catch converts it, and unwrap_or
supplies a fallback.
The error type of a declaration can be named without repeating it, using the
::err projection — ::result does the same for the success type:
error LoadError { Missing, Corrupt }
fn load(id: int): string ! LoadError {
if id < 0 {
error Missing
}
return "record ${id}"
}
fn explain(e: load::err): string {
match e {
Missing => return "not found"
Corrupt => return "damaged"
_ => return "unknown"
}
}
fn main(): void {
text := load(-1) catch {
err => {
println(explain(err))
return
}
}
println(text)
}The catch arm above returns, so text is a plain string. An arm that
produces a value instead rewrites the error channel and leaves another
Result — see Catch.
Note the binding name: error is a keyword, so an arm cannot bind error —
use err or e. Full detail lives in Errors.
Effect rows
A signature may declare the effects its body performs, in square brackets
after using. The five effects are Suspend, Error, Alloc, Spawn, and
Cancel.
error TaskError { Cancelled }
fn allocating(): []int {
return [1, 2, 3]
}
fn failing(v: int): int ! TaskError {
if v < 0 {
error Cancelled
}
return v
}
fn concurrent(): int {
left := spawn { 20 }
right := spawn { 22 }
return left.await() + right.await()
}
fn main(): void {
println("${allocating().len()} ${concurrent()} ${failing(1).unwrap_or(0)}")
}A row is a contract for readers and for API review. When it is omitted the compiler infers the union transitively through the call graph, and a row that under-states what the body reaches produces a warning rather than blocking the build — so write the row on exported functions, where the diagnostic is the point.
Generics
Type parameters go in square brackets after the name. Bounds are written
inline or in a where clause; the two forms mean the same thing.
fn identity[T](value: T): T => value
fn larger[T: Comparable[T]](a: T, b: T): T {
if a > b { return a }
return b
}
fn best[T](xs: []T): T? where T: Comparable[T] {
return xs.max()
}
fn pair_up[T, U](a: T, b: U): (T, U) => (a, b)
fn main(): void {
println("${identity(7)} ${larger(2, 9)} ${best([3, 9, 4]) ?? 0}")
println("${identity("text")} ${larger("alpha", "beta")}")
p := pair_up(1, "one")
println("${p.0}=${p.1}")
}Every call selects its own substitution: a call with int does not specialize
the declaration for a later call with string.
A bound is a promise the argument type has to keep, so a generic function is
only as usable as the implementations that exist. Display is instructive:
the prelude implements it for string and bool, and a type gains it by
writing the impl. String interpolation of an int goes through a builtin
path rather than that trait, so a T: Display bound will not accept one.
struct Tag { name: string }
impl Display for Tag {
fn to_string(self): string => "#${self.name}"
}
fn labelled[T](value: T): string where T: Display => "value=${value.to_string()}"
fn main(): void {
println(labelled(Tag { name: "release" }))
println(labelled("plain"))
println(labelled(true))
}See Generics.
Methods
A function whose first parameter is self or mut self is a method. It can
be declared directly in the type body.
struct Counter {
value: int
step: int
fn current(self): int => self.value
fn increment(mut self): void {
self.value += self.step
}
fn advanced(self, times: int): Counter =>
Counter { value: self.value + self.step * times, step: self.step }
}
fn main(): void {
mut c := Counter { value: 0, step: 5 }
c.increment()
c.increment()
println("${c.current()} ${c.advanced(3).value}")
}| Receiver | Capability |
|---|---|
no self |
static function, called through the type |
self |
read fields and call non-mutating behaviour |
mut self |
assign through the receiver |
A function inside a type body without self is static:
struct Point {
x: float
y: float
fn zero(): Point => Point { x: 0.0, y: 0.0 }
fn unit_x(): Point => Point { x: 1.0, y: 0.0 }
}
fn main(): void {
o := Point.zero()
println("${o.x} ${Point.unit_x().x}")
}The same methods can come from an impl block:
struct Vec2 { x: float, y: float }
impl Vec2 {
fn zero(): Vec2 => Vec2 { x: 0.0, y: 0.0 }
fn length(self): float => (self.x * self.x + self.y * self.y).sqrt()
fn scaled(self, k: float): Vec2 => Vec2 { x: self.x * k, y: self.y * k }
}
fn main(): void {
v := Vec2 { x: 3.0, y: 4.0 }
println("${v.length()} ${v.scaled(2.0).y} ${Vec2.zero().x}")
}Write the concrete type name as the result, not Self. Self does not unify
with the implementing type in an inherent impl:
struct Vec2 { x: float, y: float }
impl Vec2 {
fn scaled(self, k: float): Self => Vec2 { x: self.x * k, y: self.y * k }
}
fn main(): void {
println("${Vec2 { x: 1.0, y: 1.0 }.scaled(2.0).x}")
}Receiver-prefix functions
A method can also be declared at the top level by prefixing the receiver type to the name. This attaches behaviour from the module that needs it, without reopening the type declaration.
struct Reading { celsius: float }
fn Reading.fahrenheit(self): float => self.celsius * 1.8 + 32.0
fn Reading.warmer(self, by: float): Reading => Reading { celsius: self.celsius + by }
fn Reading.reset(mut self): void {
self.celsius = 0.0
}
fn main(): void {
mut r := Reading { celsius: 20.0 }
println("${r.fahrenheit()} ${r.warmer(5.0).celsius}")
r.reset()
println("${r.celsius}")
}The receiver-prefix form needs a self parameter. A fn Type.name() with no
receiver parses and type-checks as a static function, but the backend cannot
lower a call to it:
struct Reading { celsius: float }
fn Reading.freezing(): Reading => Reading { celsius: 0.0 }
fn main(): void {
println("${Reading.freezing().celsius}")
}Declare statics in the type body or in an impl block instead — both lower
fine, and the call site is identical:
struct Reading { celsius: float }
impl Reading {
fn freezing(): Reading => Reading { celsius: 0.0 }
fn boiling(): Reading => Reading { celsius: 100.0 }
}
fn Reading.fahrenheit(self): float => self.celsius * 1.8 + 32.0
fn main(): void {
println("${Reading.freezing().fahrenheit()} ${Reading.boiling().fahrenheit()}")
}Call syntax is value.method() or Type.function() regardless of which of
the three declaration sites was used. Trait implementations are covered in
Implementations.
Function values
fn(A, B) -> R is the type of a function value. Named functions and closures
both inhabit it.
fn double(value: int): int => value * 2
fn triple(value: int): int => value * 3
fn apply(value: int, operation: fn(int) -> int): int {
return operation(value)
}
fn apply_all(value: int, operations: []fn(int) -> int): []int {
mut out: []int = []
for op in operations {
out.add(op(value))
}
return out
}
fn main(): void {
println("${apply(21, double)} ${apply(21, v => v + 1)}")
results := apply_all(2, [double, triple])
println("${results[0] ?? 0} ${results[1] ?? 0}")
}Function values are ordinary values: they go in struct fields, get returned, and get selected at run time.
struct Handler {
name: string
run: fn(int) -> int
}
fn pick(mode: string): fn(int) -> int {
if mode == "double" {
return v => v * 2
}
return v => v + 1
}
fn main(): void {
h := Handler { name: "double", run: pick("double") }
println("${h.name} ${h.run(21)} ${pick("inc")(41)}")
}A function type may also be fallible. Compatibility covers parameter types,
the success type, and the error type — a closure is never erased into an
untyped callback. A value of fallible function type has the Result spelled
out, because that is what a call to it produces:
error MathError { DivideByZero }
fn checked_div(a: int, b: int): int ! MathError {
if b == 0 {
error DivideByZero
}
return a / b
}
fn checked_mod(a: int, b: int): int ! MathError {
if b == 0 {
error DivideByZero
}
return a % b
}
fn run(op: fn(int, int) -> Result[int, MathError], a: int, b: int): string {
value := op(a, b) catch {
err => return "error"
}
return "${value}"
}
fn main(): void {
println(run(checked_div, 10, 2))
println(run(checked_mod, 10, 3))
println(run(checked_div, 10, 0))
}The T ! E spelling belongs to a declaration’s return type; Result[T, E] is
the type of the value that declaration hands back, and so is what a fn(...) -> ... type needs.
Closures are documented in Closures; call mechanics in Calls.
A complete example
Defaults, methods, receiver-prefix functions, a fallible signature, and a function parameter in one file:
const DEFAULT_WIDTH: int = 24
error ReportError { NoSamples, Negative }
struct Series {
label: string
points: []float
fn total(self): float {
mut sum := 0.0
for p in self.points {
sum += p
}
return sum
}
fn mean(self): float ! ReportError {
if self.points.is_empty() {
error NoSamples
}
return self.total() / self.points.len().to_float()
}
}
fn Series.peak(self): float? => self.points.max()
fn bar(fraction: float, width: int = DEFAULT_WIDTH): string {
mut filled := (fraction * width.to_float()).to_int()
if filled < 0 { filled = 0 }
if filled > width { filled = width }
mut out := ""
for i in 0..width {
if i < filled { out += "#" } else { out += "." }
}
return out
}
fn render(s: Series, scale: fn(float) -> float): string ! ReportError {
avg := s.mean()?
if avg < 0.0 {
error Negative
}
top := s.peak() ?? 1.0
return "${s.label} ${bar(scale(avg) / top)} avg=${avg}"
}
fn main(): void {
series := [
Series { label: "cpu", points: [0.2, 0.6, 0.4] },
Series { label: "mem", points: [] },
]
for s in series {
line := render(s, v => v) catch {
err => {
println("${s.label} (no data)")
continue
}
}
println(line)
}
}API boundaries
Bodies can stay expression-oriented and heavily inferred. Exported signatures should do the opposite — the following are all compatibility surface, and a caller can break on any of them:
- parameter order, types, and
mut; - the success type and the error type;
- parameter defaults, because existing calls keep compiling and change behaviour;
- generic parameters and their bounds;
- the declared effect row.
When several optional settings start evolving together, replace the trailing default suffix with an options struct: adding a field is then an additive change instead of a new positional parameter.