Inference
+ requires Ints, so the type system infers two Int parameters and an Int result. The body has empty effect and exception rows, so its type is pure and total: pure fn!. Whether those empty rows were inferred or sealed explicitly is not part of type identity.
let addadd: pure fn!(Int, Int) -> Int = (xx: Int, yy: Int) => xx: Int + yy: Int;
add(5, 7) is enough. x and y are Int; see inference.
Infers Parameters And Returns
✓addadd: pure fn!(Int, Int) -> Int: pure fn!(Int, Int) -> Int;
expect ✓addadd: pure fn!(Int, Int) -> Int(11: Int, 22: Int) == 33: Int;
expect ✓addadd: pure fn!(Int, Int) -> Int(55: Int, 77: Int) == 1212: Int;Declare the data once. Functions do not restate it.
type User = {
name: String;
age: Int;
}
let namename: fn(user: Book.Functions.User) -> String = fn(useruser: Book.Functions.User) { useruser: Book.Functions.User.namename: String };
Fields Infer The Parameter
✓namename: fn(user: Book.Functions.User) -> String: pure fn!(User) -> String;
let adaada: Book.Functions.User = { name = "Ada""Ada": String; age = 3636: Int; };
expect ✓namename: fn(user: Book.Functions.User) -> String(adaada: Book.Functions.User) == "Ada""Ada": String;Defining functions
Functions are lambdas bound with let. There is no fn name(...) declaration. Tiny helpers use (params) => expr. Function types use fn(...) -> T — or pure fn!(...) -> T when purity and totality are known. => never appears in type position.
let doubledouble: fn(x: Int) -> Int = fn(xx: Int) { xx: Int * 22: Int };
Named helpers use fn(params) { ... }. Locals are ordinary let; the last expression is the result.
let plusplus: fn(x: Int, y: Int) -> Int = fn(xx: Int, yy: Int) {
let zz: Int = xx: Int;
zz: Int + yy: Int
};
An optional return type follows the parameters. return is an early exit, not the ordinary way to produce a value.
let firstfirst: fn(items: Vector(Int)) -> Int = fn(itemsitems: Vector(Int)) -> Int {
if Vector.is_emptyis_empty: pure fn!('a)(items: Vector(Int)) -> Bool(itemsitems: Vector(Int)) {
return 00: Int;
}
itemsitems: Vector(Int)[00: Int]
};
Braces after => are values, never statement blocks.
type Gate = {
deny: Bool;
}
let denydeny: fn() -> Book.Functions.Gate = () =>
Gate { deny = TrueTrue: Bool; };
Unary match
A unary match helper uses function { | ... }. See pattern matching.
type Status =
| Open
| Done(String)
| Archived
let classifyclassify: fn(Book.Functions.Status) -> String = function {
| Open -> "open""open": String
| Done(atat: String) -> "done at ""done at ": String ++ atat: String
| Archived -> "archived""archived": String
};
Unary Match
expect ✓classifyclassify: fn(Book.Functions.Status) -> String(Open) == "open""open": String;
expect ✓classifyclassify: fn(Book.Functions.Status) -> String(Done("2026-06-12""2026-06-12": String)) == "done at 2026-06-12""done at 2026-06-12": String;
expect ✓classifyclassify: fn(Book.Functions.Status) -> String(Archived) == "archived""archived": String;Recursion
let rec binds a function that can call itself. Recursive bindings need a type on the head.
let rec lengthlength: fn(items: List(Int)) -> Int, exn [ 'a ], eff 'b = (itemsitems: List(Int): List(Int)) -> Int =>
match itemsitems: List(Int) {
| [] -> 00: Int
| [_, ..tailtail: List(Int)] -> lengthlength: fn(items: List(Int)) -> Int, exn [ 'a ], eff 'b(tailtail: List(Int)) + 11: Int
};
Recursion
expect ✓lengthlength: fn(items: List(Int)) -> Int, exn [ 'a ], eff 'b(listlist: pure fn!('a)(...Int) -> List(Int)()) == 00: Int;
expect ✓lengthlength: fn(items: List(Int)) -> Int, exn [ 'a ], eff 'b(listlist: pure fn!('a)(...Int) -> List(Int)(11: Int, 22: Int, 33: Int)) == 33: Int;Labeled arguments
Labeled parameters use ~name in the definition. Calls pass name = value without ~. Runtime defaults belong only to labeled parameters; inferred static struct witnesses are the exception. There is no * separator or trailing (). Label order remains part of a callback type.
let surroundsurround: fn(value: String, ~left: String, ~right: String) -> String = fn(valuevalue: String, ~leftleft: String, ~rightright: String) { leftleft: String ++ valuevalue: String ++ rightright: String };
~name = default may be omitted at the call. Defaults evaluate afresh on every invocation, never when the function is defined. They see ordinary lexical captures and parameters already initialized to their left.
let describedescribe: fn(value: Int, ~label: String, ~suffix: String = "") -> String = fn(valuevalue: Int, ~labellabel: String, ~suffixsuffix: String = """": String) {
labellabel: String ++ ":"":": String ++ Int.to_stringto_string: pure fn!(value: Int) -> String(valuevalue: Int) ++ suffixsuffix: String
};
let offsetoffset: fn(value: Int, ~amount: Int = 1 + 2) -> Int = fn(valuevalue: Int, ~amountamount: Int = 11: Int + 22: Int) { valuevalue: Int + amountamount: Int };
Labeled Calls
✓surroundsurround: fn(value: String, ~left: String, ~right: String) -> String: pure fn!(String, ~left: String, ~right: String) -> String;
expect ✓surroundsurround: fn(value: String, ~left: String, ~right: String) -> String("ship""ship": String, left = "[""[": String, right = "]""]": String) == "[ship]""[ship]": String;
✓describedescribe: fn(value: Int, ~label: String, ~suffix: String = "") -> String: pure fn!(Int, ~label: String, ~suffix: String = "") -> String;
expect ✓describedescribe: fn(value: Int, ~label: String, ~suffix: String = "") -> String(33: Int, label = "n""n": String) == "n:3""n:3": String;
expect ✓describedescribe: fn(value: Int, ~label: String, ~suffix: String = "") -> String(33: Int, label = "n""n": String, suffix = "!""!": String) == "n:3!""n:3!": String;
✓offsetoffset: fn(value: Int, ~amount: Int = 1 + 2) -> Int: pure fn!(Int, ~amount: Int = 1 + 2) -> Int;
expect ✓offsetoffset: fn(value: Int, ~amount: Int = 1 + 2) -> Int(44: Int) == 77: Int;
expect ✓offsetoffset: fn(value: Int, ~amount: Int = 1 + 2) -> Int(44: Int, amount = 11: Int) == 55: Int;A call initializes every parameter in declaration order. Each slot evaluates either its supplied expression or its default expression when reached. Call-site label order never changes evaluation order, except in a call with a spread, which evaluates its written arguments left to right.
Default Evaluation Order
let tracetrace: Book.Functions.CallTrace = CallTrace { digits = 00: Int; };
let pairpair: fn(~first: String = mark(trace, 1, "first"), ~second: String = mark(
trace,
if first == "first" {
| 2
| 8
},
"second",
)) -> String = fn(
~firstfirst: String = markmark: fn(trace: Book.Functions.CallTrace, digit: Int, value: String) -> String(tracetrace: Book.Functions.CallTrace, 11: Int, "first""first": String),
~secondsecond: String = markmark: fn(trace: Book.Functions.CallTrace, digit: Int, value: String) -> String(
tracetrace: Book.Functions.CallTrace,
if firstfirst: String == "first""first": String {
| 22: Int
| 88: Int
},
"second""second": String,
),
) {
firstfirst: String ++ "|""|": String ++ secondsecond: String
};
let suppliedsupplied: String = pairpair: fn(~first: String = mark(trace, 1, "first"), ~second: String = mark(
trace,
if first == "first" {
| 2
| 8
},
"second",
)) -> String(second = markmark: fn(trace: Book.Functions.CallTrace, digit: Int, value: String) -> String(tracetrace: Book.Functions.CallTrace, 33: Int, "given second""given second": String));
expect ✓(tracetrace: Book.Functions.CallTrace.digitsdigits: Int, suppliedsupplied: String) == (1313: Int, "first|given second""first|given second": String);
trace.digits := 00: Int;
let reversedreversed: String = pairpair: fn(~first: String = mark(trace, 1, "first"), ~second: String = mark(
trace,
if first == "first" {
| 2
| 8
},
"second",
)) -> String(
second = markmark: fn(trace: Book.Functions.CallTrace, digit: Int, value: String) -> String(tracetrace: Book.Functions.CallTrace, 22: Int, "second""second": String),
first = markmark: fn(trace: Book.Functions.CallTrace, digit: Int, value: String) -> String(tracetrace: Book.Functions.CallTrace, 11: Int, "first""first": String),
);
expect ✓(tracetrace: Book.Functions.CallTrace.digitsdigits: Int, reversedreversed: String) == (1212: Int, "first|second""first|second": String);
trace.digits := 00: Int;
let defaulteddefaulted: String = pairpair: fn(~first: String = mark(trace, 1, "first"), ~second: String = mark(
trace,
if first == "first" {
| 2
| 8
},
"second",
)) -> String();
expect ✓tracetrace: Book.Functions.CallTrace.digitsdigits: Int == 1212: Int;
expect ✓defaulteddefaulted: String == "first|second""first|second": String;Defaults use ordinary closure capture. Capturing a value has the same locality, uniqueness, multiplicity, and sendability consequences as using that value in the function body.
Default Lexical Capture
let prefixprefix: String = "captured""captured": String;
let labellabel: fn(~value: String = prefix ++ " default") -> String = fn(~valuevalue: String = prefixprefix: String ++ " default"" default": String) { valuevalue: String };
expect ✓labellabel: fn(~value: String = prefix ++ " default") -> String() == "captured default""captured default": String;Fallible and effectful defaults contribute conservatively to the function's single exception and effect channels. Supplying the parameter explicitly skips runtime evaluation but does not change that function type.
Option labels
An Option label may be omitted and becomes None. That rule is type-directed, so the label names Option. An authored default wins over implicit None.
let queryquery: fn(sql: String, ~row_limit: Option(Int)) -> String = fn(sqlsql: String, ~row_limitrow_limit: Option(Int): Option(Int)) {
match row_limitrow_limit: Option(Int) {
| None -> sqlsql: String
| Some(nn: Int) -> sqlsql: String ++ " limit "" limit ": String ++ Int.to_stringto_string: pure fn!(value: Int) -> String(nn: Int)
}
};
let log_linelog_line: fn(~level: String = "info", message: String) -> String = fn(~levellevel: String = "info""info": String, messagemessage: String) {
"[""[": String ++ levellevel: String ++ "] ""] ": String ++ messagemessage: String
};
Option Labels
expect ✓queryquery: fn(sql: String, ~row_limit: Option(Int)) -> String("select 1""select 1": String) == "select 1""select 1": String;
expect ✓queryquery: fn(sql: String, ~row_limit: Option(Int)) -> String("select 1""select 1": String, row_limit = Some(100100: Int)) == "select 1 limit 100""select 1 limit 100": String;
let pagepage: fn(~row_limit: Option(Int) = Some(25)) -> Option(Int) = fn(~row_limitrow_limit: Option(Int): Option(Int) = SomeSome: fn(Int) -> Option(Int)(2525: Int)) { row_limitrow_limit: Option(Int) };
expect ✓pagepage: fn(~row_limit: Option(Int) = Some(25)) -> Option(Int)() == SomeSome: fn(Int) -> Option(Int)(2525: Int);
expect ✓pagepage: fn(~row_limit: Option(Int) = Some(25)) -> Option(Int)(row_limit = None) == NoneNone: Option(Int);
✓log_linelog_line: fn(~level: String = "info", message: String) -> String: pure fn!(~level: String = "info", String) -> String;
expect ✓log_linelog_line: fn(~level: String = "info", message: String) -> String("ready""ready": String) == "[info] ready""[info] ready": String;
expect ✓log_linelog_line: fn(~level: String = "info", message: String) -> String("careful""careful": String, level = "warn""warn": String) == "[warn] careful""[warn] careful": String;Callbacks
Put the value in the first positional slot. Callbacks go in labeled slots (f = ...).
let applyapply: fn(value: 'a, ~f: fn('a) -> 'b) -> 'b = fn(valuevalue: 'a, ~ff: fn('a) -> 'b) { ff: fn('a) -> 'b(valuevalue: 'a) };
Pipelines
|> fills the first positional argument. The right-hand side is a function name, a call, or parentheses around either.
Pipelines
expect ✓(55: Int |> doubledouble: fn(x: Int) -> Int) == 1010: Int;
expect ✓(55: Int |> addadd: pure fn!(Int, Int) -> Int(33: Int)) == 88: Int;
expect ✓(55: Int |> describedescribe: fn(value: Int, ~label: String, ~suffix: String = "") -> String(label = "value""value": String)) == "value:5""value:5": String;
expect ✓(55: Int |> describedescribe: fn(value: Int, ~label: String, ~suffix: String = "") -> String(label = "value""value": String, suffix = "!""!": String)) == "value:5!""value:5!": String;
expect ✓(22: Int
|> doubledouble: fn(x: Int) -> Int
|> addadd: pure fn!(Int, Int) -> Int(11: Int)
|> describedescribe: fn(value: Int, ~label: String, ~suffix: String = "") -> String(label = "chained""chained": String)) == "chained:5""chained:5": String;
expect ✓(77: Int |> (doubledouble: fn(x: Int) -> Int)) == 1414: Int;
expect ✓(4242: Int |> Int.to_stringto_string: pure fn!(value: Int) -> String) == "42""42": String;Partial application
Partial application is explicit: add(5, ...). ... must be last and must leave at least one parameter; do not mix it with |>. It selects and captures the callee immediately, but supplied parameter expressions remain unevaluated until full saturation. Every invocation evaluates those expressions afresh in declaration order.
Partial Application
✓addadd: pure fn!(Int, Int) -> Int: pure fn!(Int, Int) -> Int;
let add5add5: fn(Int) -> Int = addadd: pure fn!(Int, Int) -> Int(55: Int, ...);
expect ✓add5add5: fn(Int) -> Int(77: Int) == 1212: Int;
let labelledlabelled: fn(value: Int, ~suffix: String = "") -> String = describedescribe: fn(value: Int, ~label: String, ~suffix: String = "") -> String(label = "n""n": String, ...);
expect ✓labelledlabelled: fn(value: Int, ~suffix: String = "") -> String(33: Int) == "n:3""n:3": String;
expect ✓labelledlabelled: fn(value: Int, ~suffix: String = "") -> String(33: Int, suffix = "!""!": String) == "n:3!""n:3!": String;
let described_onedescribed_one: fn(~label: String, ~suffix: String = "") -> String = describedescribe: fn(value: Int, ~label: String, ~suffix: String = "") -> String(11: Int, ...);
expect ✓described_onedescribed_one: fn(~label: String, ~suffix: String = "") -> String(label = "one""one": String) == "one:1""one:1": String;
let from_callbackfrom_callback: Int = applyapply: fn(value: Int, ~f: fn(Int) -> Int) -> Int(1010: Int, f = addadd: pure fn!(Int, Int) -> Int(3232: Int, ...));
expect ✓from_callbackfrom_callback: Int == 4242: Int;Deferred Partial Arguments
let tracetrace: Book.Functions.CallTrace = CallTrace { digits = 00: Int; };
let pairpair: fn(~first: String = mark(trace, 1, "default first"), ~second: String = mark(trace, 2, "default second")) -> String = fn(
~firstfirst: String = markmark: fn(trace: Book.Functions.CallTrace, digit: Int, value: String) -> String(tracetrace: Book.Functions.CallTrace, 11: Int, "default first""default first": String),
~secondsecond: String = markmark: fn(trace: Book.Functions.CallTrace, digit: Int, value: String) -> String(tracetrace: Book.Functions.CallTrace, 22: Int, "default second""default second": String),
) {
firstfirst: String ++ "|""|": String ++ secondsecond: String
};
let choosechoose: fn() -> fn(~first: String = mark(trace, 1, "default first"), ~second: String = mark(trace, 2, "default second")) -> String = fn() {
trace.digits := 99: Int;
pairpair: fn(~first: String = mark(trace, 1, "default first"), ~second: String = mark(trace, 2, "default second")) -> String
};
let pendingpending: fn(~first: String = mark(trace, 1, "default first")) -> String = (choosechoose: fn() -> fn(~first: String = mark(trace, 1, "default first"), ~second: String = mark(trace, 2, "default second")) -> String())(second = markmark: fn(trace: Book.Functions.CallTrace, digit: Int, value: String) -> String(tracetrace: Book.Functions.CallTrace, 44: Int, "supplied second""supplied second": String), ...);
expect ✓tracetrace: Book.Functions.CallTrace.digitsdigits: Int == 99: Int;
let first_runfirst_run: String = pendingpending: fn(~first: String = mark(trace, 1, "default first")) -> String();
expect ✓tracetrace: Book.Functions.CallTrace.digitsdigits: Int == 914914: Int;
expect ✓first_runfirst_run: String == "default first|supplied second""default first|supplied second": String;
let secondsecond: String = pendingpending: fn(~first: String = mark(trace, 1, "default first")) -> String();
expect ✓(tracetrace: Book.Functions.CallTrace.digitsdigits: Int, secondsecond: String) == (9141491414: Int, "default first|supplied second""default first|supplied second": String);Collection callback example
Stdlib helpers take the collection first and the callback as f = ....
Callbacks
let namesnames: Vector(String) = ["Ada""Ada": String, "Grace""Grace": String];
let shoutedshouted: Vector(String) = Vector.mapmap: pure fn!('a, 'b)(items: Vector(String) @ 'm, ~f: fn(value: String @ 'm) -> String) -> Vector(String)(namesnames: Vector(String), f = (namename: String) => namename: String ++ "!""!": String);
expect ✓Vector.getget: pure fn!('a)(items: Vector(String), index: Int) -> String(shoutedshouted: Vector(String), 00: Int) == "Ada!""Ada!": String;
expect ✓Vector.getget: pure fn!('a)(items: Vector(String), index: Int) -> String(shoutedshouted: Vector(String), 11: Int) == "Grace!""Grace!": String;Outcome pipelines
>> attaches stages to a function. The result is still a function with the same parameters, and each stage shapes what the function produces: its value, its errors, or how it runs.
The simplest stage is an outcome handler: a function whose arms are all concrete error :tag patterns. Success passes through, every listed tag is handled, and every other error propagates unchanged.
let lookuplookup: fn(id: Int) -> String, exn [ :db(String) | :missing ] = fn(idid: Int: Int) {
match idid: Int {
| 1 -> "Ada""Ada": String
| 2 -> error :missing
| _ -> error :db("down""down": String)
}
};
let db_to_storedb_to_store: Unit = function {
| error :db(_) -> error :store("database")
};
Outcome Handlers
let findfind: fn(id: Int) -> String, exn [ :missing | :store(String) ] = fn(idid: Int: Int) { lookuplookup: fn(id: Int) -> String, exn [ :db(String) | :missing ](idid: Int) } >> db_to_store;
✓findfind: fn(id: Int) -> String, exn [ :missing | :store(String) ]: fn(Int) -> String, exn [ :missing | :store(String) ];
expect ✓must findfind: fn(id: Int) -> String, exn [ :missing | :store(String) ](11: Int) == "Ada""Ada": String;
expect ✓match findfind: fn(id: Int) -> String, exn [ :missing | :store(String) ](33: Int) {
| namename: String -> namename: String
| error :store(messagemessage: String) -> messagemessage: String
| error :missing -> "missing""missing": String
} == "database""database": String;Stages run in order, so a later stage sees what earlier ones raise. A handler written in place drops function { } and lists its arms right after >>. Each arm ends at the next | or >>; when a statement ends on several arms, its ; takes its own line. A function whose body is a handler is a template; its arguments are evaluated only when one of its arms fires. Any |> right-hand side is a map that runs on success, which is useful when several outcomes need the same final shaping.
let store_asstore_as: Unit = (label: String) =>
function {
| error :db(_) -> error :store(label)
};
Stage Order
let name_lengthname_length: fn(id: Int) -> Int = fn(idid: Int: Int) { lookuplookup: fn(id: Int) -> String, exn [ :db(String) | :missing ](idid: Int) } >> store_as("lookup""lookup": String)
>> | error :store(messagemessage: String) -> messagemessage: String
| error :missing -> "nobody""nobody": String
>> String.char_countchar_count: pure fn!(string: String) -> Int;
✓name_lengthname_length: fn(id: Int) -> Int: fn(Int) -> Int;
expect ✓name_lengthname_length: fn(id: Int) -> Int(11: Int) == 33: Int;
expect ✓name_lengthname_length: fn(id: Int) -> Int(22: Int) == 66: Int;
expect ✓name_lengthname_length: fn(id: Int) -> Int(33: Int) == 66: Int;A named function works on the left too; its parameters, labels included, are forwarded. return leaves the body, not the pipeline, so every stage still runs.
Named Functions And Return
let namednamed: fn(__pipeline_arg0: Int) -> String = lookuplookup: fn(id: Int) -> String, exn [ :db(String) | :missing ] >> db_to_store
>> | error :missing -> "nobody""nobody": String
| error :store(_) -> "broken""broken": String
;
expect ✓namednamed: fn(__pipeline_arg0: Int) -> String(22: Int) == "nobody""nobody": String;
let earlyearly: fn(id: Int) -> Int = fn(idid: Int: Int) {
if idid: Int == 00: Int {
return "zero""zero": String;
}
lookuplookup: fn(id: Int) -> String, exn [ :db(String) | :missing ](idid: Int)
} >> db_to_store
>> | error :missing -> "nobody""nobody": String
| error :store(_) -> "broken""broken": String
>> String.char_countchar_count: pure fn!(string: String) -> Int;
expect ✓earlyearly: fn(id: Int) -> Int(00: Int) == 44: Int;
expect ✓earlyearly: fn(id: Int) -> Int(33: Int) == 66: Int;A With manager wraps everything before it. A Drive value, such as Retry.policy or Timeout.after, runs everything before it as its body, possibly more than once.
Handlers exist only as stages: using db_to_store as a value is an error. A wrapped function cannot declare its own result type, because its stages decide the result and its exn row; annotate the binding instead. See rejected programs.