Functions and Closures
Declaration and calls
Parameters pair names with types; the return type follows a colon. A function without a return type returns Void. Arguments evaluate left to right. A function declaring : Int returns an Int on every path; any other type is an E304 error with expected/got labels.
fn add(a: Int, b: Int): Int {
return a + b;
}
fn greet(name: String) {
print("hello,", name);
}
greet("ore");
print(add(20, 22));Return type inference applies where the body determines the type; explicit annotations are required on pub signatures and recommended everywhere a mismatch would otherwise surface at a distance.
Default values and named arguments
Trailing parameters declare defaults, filled in when the caller omits them. Any argument may pass by name after the positional ones. A mandatory parameter after a defaulted one is an error, as are unknown or duplicate names. Methods follow the same rules. Defaults compile away: the callee always receives plain positional values.
fn configure(host: String, port: Int = 8080, secure: Bool = false): String {
let sec = "http";
if (secure) {
sec = "https";
}
return "${sec}://${host}:${port}";
}
print(configure("api.dev"));
print(configure("api.dev", secure: true));
print(configure("api.dev", port: 9000, secure: true));Generic functions
Type parameters in angle brackets solve from argument types at each call, or pass explicitly. Parameters erase to a shared representation, so one definition serves every type. A parameter that cannot be inferred, or two arguments forcing different types onto one parameter, is a compile error naming the parameter.
fn identity<T>(val: T): T {
return val;
}
print(identity(42));
print(identity("hi"));Lambdas and closures
A lambda is (params) => body, with the single-identifier shorthand x => body. The body is one expression (implicit return) or a block. fn(...) => ... is rejected with E105: fn declares named functions with block bodies only.
let factor = 10;
let mult = (x: Int) => x * factor;
assert(mult(5) == 50, "capture");Closures capture their environment by value and are first-class: they pass as arguments, store in fields, and call anywhere. Captures keep heap values alive. Parameter annotations may be omitted when the call site determines the shape: map/filter infer the element type from the receiver, and plain functions infer from fn-typed parameters.
fn apply(x: Int, f: fn(Int): Int): Int {
return f(x);
}
let nums = [1, 2, 3];
assert(nums.map((n) => n * 2)[2] == 6, "inferred map");
assert(apply(5, (n) => n * 2) == 10, "inferred HOF");A closure stored on its own object requires fn decay(this); see Cycles and Handles.
Methods
Functions defined inside a class or struct take an implicit receiver and call with dot syntax. Inside the body, this refers to the receiver. init(params) runs at construction (new Meter(20) calls it; .init() is never written). static methods call on the type itself with no receiver.
class Meter {
let reading: Int;
init(reading: Int) {
this.reading = reading;
}
fn bump(amount: Int): Int {
this.reading = this.reading + amount;
return this.reading;
}
}
let m = new Meter(20);
print(m.bump(22));throws and try..catch
A function that can fail marks throws, bare, with no error set. Callers are inside try or declare throws themselves; the checker enforces the contract. throw carries any value. A bare throw; rethrows the current error and is legal only directly inside catch.
fn load(ok: Bool): Int throws {
if ok { return 42; }
throw 0;
}
try {
print(load(true));
} catch (err) {
print("failed");
}Calling a throws function from another function propagates on every backend through a per-thread error slot; the nearest enclosing catch receives the payload.
try accepts an optional single catch (id) plus an optional finally. The finally block runs on normal exit, error return, and nested rethrow alike; it desugars to a synthetic defer and shares those exact semantics. defer blocks registered between a throw and its catch run first, innermost first.
fn Main(): Int {
let log: Array<String> = [];
try {
defer log.push("inner");
throw "down";
} catch (err) {
log.push("caught: " + err);
}
print(log[0]);
print(log[1]);
return 0;
}A throw propagates on every backend, across function boundaries, through a per-thread error slot. Thrown class instances keep their identity: is checks match (including ancestors) and narrow the binding, so fields are accessible in the arm. An error that escapes all handlers fails the entry call. switch over the caught value with case is Type: arms dispatches the same way.
class IoError {
let message: String;
init(message: String) { this.message = message; }
}
class ParseError {
let message: String;
init(message: String) { this.message = message; }
}
fn first(): String throws {
throw new ParseError("bad header");
}
fn Main(): Int {
try {
print(first());
} catch (err) {
switch err {
case is IoError:
print("caught io");
pass;
case is ParseError:
print("caught parse");
pass;
default:
print("caught other");
pass;
}
}
return 0;
}Functions that cannot usefully decide an error declare throws and propagate; handling belongs at the boundary where retry, default, or abort-with-context is available.
Nullable and Result channels: ? ?? ?.
Result values unwrap with postfix ?. Ok yields the payload; Err returns early carrying the error, so the enclosing function must return a Result. ? binds tighter than binary operators and never steals ternary colons. Applying ? to a non-Result value, or inside a function returning something else, is a compile error.
fn popIt(): Result<Int, String> {
let arr = [10, 20];
let v = arr.pop() ?? -1;
return Result.Ok(v + 1);
}
print(popIt().unwrap());lhs ?? rhs yields lhs when it is not null and evaluates rhs otherwise; rhs never runs when lhs is present. Either side may be nullable, so fallbacks chain.
let port: Int? = null;
assert((port ?? 8080) == 8080, "fallback");
let a: Int? = null;
let b: Int? = 42;
assert((a ?? b ?? 8080) == 42, "chain");target?.field and target?.method(args) short-circuit a chain: a null anywhere yields null without touching the rest. ?. must directly touch its operands.
record Profile(name: String)
record User(profile: Profile?)
let user = User(Profile("Al"));
assert(user?.profile?.name == "Al", "chain");
let ghost = User(null);
assert((ghost?.profile?.name ?? "anonymous") == "anonymous", "fallback");Test functions
Functions marked test fn take no parameters, return nothing, and are stripped from normal builds. The test runner discovers them, times them, and reports each one. assert takes a Bool and a message. Test execution is specified in Project and Toolchain.
test fn adds_up() {
assert(1 + 1 == 2, "math");
}Summary
fn name(params): Retwith block bodies andreturn;(params) => bodylambdas capturing by value.- Trailing defaults with positional-or-named calls; generics solved per call.
- Methods use
this;initconstructs;staticneeds no receiver. throwsmarks fallibility;try/catch/finallyhandle;throwcarries values.?early-returns throughResult;??falls back;?.reaches through.test fnmarks tests; mismatched returns areE304.