fn(name) { … } and Takes the Arguments It Names; a Parameter That Is Code Has the Type fn(A) -> R, and Its Shape Follows the Type and Whether the Body Keeps ItStatus: Accepted
Date: October 3, 2026
History: the records this one replaces are archived in docs/history/adr; read them only for how the decision was reached
Answers: how a lambda is written and how many arguments it takes (the
silent fn: misparse; the review of the automatic a, b, c); how a
package declares a parameter that is code, and what such a parameter may do
(examples/http/ could not declare route); how such a parameter is lowered
when the code may pause (#187),
and the former open-decisions.md question does the body’s
run-or-kept answer decide a function-typed parameter’s lowering?, which the
owner answered with its option A
Related:
ADR-013 D5 (fn: as a chaining limitation, displaced by D1 here);
ADR-289 D17 (a handler is a lambda);
ADR-288 (a written sync is an assertion; NK2202);
ADR-288 D15, D16 (sync = "from(f)" for a lambda run during the call, and its limit);
ADR-024 (NK1117, which refuses an undeclared a);
ADR-094 D1, D2 (the keeps column, and the callee’s body deciding the caller’s &; D17 applies D2 to a trait method);
ADR-037 D7, ADR-283 D2 (one written type, a representation the analysis picks);
ADR-140 D4 (sync and throws after the result, in a declaration as in a type);
ADR-141 (a lambda carries no promise word);
ADR-217 D2 (the kept lowering, nikaia_std::func::Kept; NK1142 retired);
ADR-230 (what a function field’s stored code reaches, for the lock column);
ADR-233 (a pausing lambda handed to std’s pausing counterpart);
ADR-288 D31 (sync(f), the written form of pauses only where its lambda does);
ADR-296 D12 (measure before choosing a shape);
ADR-055 (a pausing function is an async fn)
How is code passed as a value: how is a lambda written and how many arguments does it take; how does a function declared in this language say that a parameter is code and what that code may do; and what does such a parameter become in the language below, given that the code handed to it may pause?
Without an answer, a lambda’s body could swallow the chain after it, a local
named a could silently change a lambda’s arity, a package could not take a
handler, comparator or hook, and a pausing lambda had no lowering at a
parameter that only runs it.
| Language / tool | Its answer | What it costs there | Source |
|---|---|---|---|
| Kotlin, Ruby | an expression-bodied block whose body ends at the end of the line | the language is newline-sensitive at that point | from memory |
| Rust (stable, 1.94.1, edition 2021) | async \|x\| { … } closures and AsyncFn/AsyncFnMut/AsyncFnOnce bounds, no feature gate |
AsyncFn is a bound, not a type: a value stored in a field needs a box or a type parameter |
measured (D13) |
Rust std |
Iterator::map takes FnMut; an async closure handed to it yields an iterator of futures |
a different program, not a pausing map |
D16 |
rayon |
rayon::join takes synchronous closures |
nothing can await inside one |
D16 |
| Nikaia’s own precedent | one written type, a representation chosen per value or per site: Shared[T] is Rc or Arc per value, a struct has up to three layouts, the callee’s body decides the caller’s & |
a reader consults the ledger for the representation | ADR-037 D7, ADR-283 D2, ADR-094 D1 |
What does not transfer: Rust’s closure types. A Nikaia function type is
written once and says what the code may do (sync, throws); which Rust shape
carries it is the compiler’s choice.
| Question | Option | Why it lost, or won |
|---|---|---|
| Short lambda | keep fn: expr |
silent misparse of a following .method(); two spellings of one thing; a body that cannot grow (D1, D5) |
fn: body ends at end of line |
newline sensitivity in one place, for four characters (D1) | |
| restrict the body so no chain follows | no restriction keeps a.message.cmp(b.message) and refuses .listen(…) |
|
keep fn:, forbid a chain after it |
keeps every other cost and adds a rule | |
| lambda only inside the parentheses | unambiguous, still allowed, but a chain of trailing lambdas reads better (Part II 12.6, Part III 17.1) | |
| Arguments | automatic a, b, c, arity read off the body |
a local named a becomes an argument; every lambda depends on the rule (D2) |
keep them, warned (NK1114) |
visible is not good; the rule stays alive with the warning (D2) | |
| announce the withdrawal a release ahead | protects no code outside the repository (D2) | |
| Run or kept | a written @detached |
a word the body already answers; the ledger records it (D8) |
| Pausing parameter’s shape | impl Fn(A) -> R for a run parameter |
a pausing lambda cannot be written into one (D10, D11) |
| boxed future for run and kept alike | 11.99 ns/call against 1.37 for impl AsyncFn, on a 0.31 floor (D13); kept only where the code is stored (D11) |
|
impl AsyncFn by the body’s keeps answer (D11) |
chosen: the run case pays 4.4× the floor instead of 38×; the cost is predictability, answered by the ledger (D15) |
fn: expression is removed.A lambda is fn(name, …) { … }; the block is the body whether it holds one
line or several. A block ends at its }, so nothing after it can be swallowed
into it.
fn(user) { … } takes one, fn(acc, n) { … } two. A lambda that names none
takes none: fn { … } is the zero-argument form, which is what
.or_insert_with fn { Stats(0) } takes. There are no automatic names: a body
that uses a, b or c without declaring it names something nothing
declares and meets NK1117, with no refusal of its own. NK1114 is retired
and not reused.
Where there are no other arguments the parentheses go with it:
users.map fn(user) { user.id }. This is where the argument goes, not a second
spelling, and it is what lets a chain read down the page.
.route("/x") fn { handler(db) } is one call, and the chain after the } is
the outer chain:
Server()
.route("/x") fn { handler(db) }
.listen(":8080")
fn: is an error that says what happened.The message names the block form and the reason the short one went: its body
ran to the end of the expression, so a .method() after it landed inside the
lambda.
fn(A, B) -> R is a type, written the way a signature is.Parameters in parentheses, -> R where there is a result, sync and throws
after the result:
fn route(&mut self, path: &str, handler: fn(Request) -> Response) // may pause
fn on_tick(&mut self, handler: fn() sync) // never pauses
fn load(&mut self, reader: fn(Path) -> Bytes throws) // may fail
A function type stands wherever a type does: a parameter, a field, a result, a
let, an element. The trailing words are greedy: in
fn make() -> fn(i64) -> i64 sync the sync belongs to the result type. A
lambda itself carries no promise word; the promise is on the type it is handed
to.
Without sync the code may pause; with it, it never does. Without throws it
cannot fail; with it, it may. A lambda that does less fits a type that allows
more. The other direction is refused in the shape of the refusal one level
down: a pausing lambda handed to a fn() sync is NK2206 (NK2202’s shape),
a failing one handed to a type without throws is NK2606 (NK2605’s shape),
reported alone, since the function around the lambda is not the one that has to
answer for it.
keeps.touches and throws count for the callee. Its
pausing is the type’s: where the type may pause the lambda is awaited, so the
callee may pause; a function that promises to pause
only where its lambda does writes sync(f) (ADR-288 D31).Nothing is written for it; there is no @detached. The ledger’s existing
keeps column records it: fn runs(f: fn() -> String) { return f() } carries
no keeps, fn holds(f: fn() -> String) { spawn fn { f() } } carries
keeps = ["f"]. A body that stores and runs gets the kept answer.
The type carries sync and throws and no more, so a stored handler’s
touches is everything and its locks may — the fail-closed answers for an
undescribed call. A run lambda is unchanged: its body is in front of the check.
Where a struct literal fills a function field, the lock column reads what is
stored there instead (ADR-230 D1).
There is no refusal of a pausing lambda at a run parameter declared in this language.
keeps answer.| parameter’s type | run | kept |
|---|---|---|
says sync |
plain closure, impl Fn(A) -> R |
Kept of a plain closure (ADR-217 D2) |
| may pause | impl AsyncFn(A) -> R; the lambda is async \|a\| { … }, with no move |
Kept of a closure returning a boxed future (ADR-217 D2) |
AsyncFn is a bound, and a value a field holds needs a type, which is why the
kept case keeps the box. Where the type says throws, the closure’s result is
the Result a throws function’s is. A call to a pausing parameter in the
callee’s body carries an .await.
A run sync parameter of a function that lies on a cycle of the call graph is
written &dyn Fn(A) -> R: impl Fn would make each new lambda a new
instantiation, without end (#1023).
std’s lambda-taking entries are Rust with hand-written signatures, which take
a synchronous closure whatever the ledger’s sync column says, so D11 does not
apply to them and map and its kin pay nothing new. What a pausing lambda
handed to one of them becomes is ADR-233’s.
benches/handler keeps the rows, best of five, control tying: plain closure
0.31 ns/call, boxed future 11.99 (38×), impl AsyncFn 1.37 (4.4×).
A premise that rules a shape out is checked too: stable Rust has async
closures.
keeps column the same way, including when it is absent.A callee this build has no contract for reads as run at both ends. The
declaration writes the parameter and the call writes the argument; the two
disagreeing would be one parameter with two shapes, refused by rustc about a
file nobody wrote — the rule ADR-094 D1 states for the &.
A run parameter’s cost is not readable from its signature alone, and a library
author who moves a body from run to kept moves every caller’s cost. A keeps
change is a nikaia.contracts diff in review, and --locked fails a build
whose contracts moved unrecorded — the same instrument ADR-094 and
ADR-283 D4 use. sync on the type remains the way to ask for no
future at all.
overlap branch are blocks, not callables.Changed by ADR-async-5 D10: a task an async form starts is such a block; overlap is removed.
spawn fn { … } and overlap { … } lower to async blocks: a body is called
once, and wrapping it in a closure adds a call and takes nothing away.
task::interleave and the overlap/race families take futures because a
branch is that same block, and because rayon::join, the pool vehicle, takes
synchronous closures.
Decided by the owner, 2026-10-30 (#1012, option E).
A trait method has no body of its own to read (ADR-094 D2), so its keeps
column is the union, position by position, of its default body’s and those of
every impl of the trait in the trait’s own package, private impls included.
The column is computed inside the keeps fixpoint: the trait method is a node
with an edge to each local impl and one back, so the trait and its impls
converge to one column, and every impl, call site and lowering follows it (D11).
rustc against impl Fn for an impl written in Rust).NK1264, which names the trait method, the parameter, the statement that
keeps it, and that only the trait’s package can widen the column. An impl
that keeps less takes the column.pub trait method warns, in both directions
(NK1265, beside NK1208), naming the impl that caused it, until the
ledger is committed. Unlike a function’s keeps, a trait column breaks
someone either way: widening refuses lending callers, narrowing refuses
foreign keepers. A lending caller refused because of the column is told
which impl made the position kept.tests/ do not count: a test is a client.pub trait whose keeping impls all live
in other packages reads run and refuses them. A registry is written as a
struct with a field of fn type, which keeps by D8. How a trait would
declare the intent is decided when a program needs it.Rejected:
Arc and
'static on every trait callback; blocks the self-hosting move of
ComptimeComputeHost.ref fn on the trait’s parameter: refused by the owner (#628: ref
goes away; the ledger is the interface).Arc where nothing keeps; the default favours
the rare case.rustc forbids an impl stricter than its
trait (E0276), and generic code is lowered once for all impls.sync
is told so in this compiler’s words.touches — D9 fails closed; no program
has asked.fn[T](T) -> T.AsyncFn, AsyncFnMut or AsyncFnOnce is the right bound for a
run parameter.AsyncFn; nothing in
the surface changes for the lowering.