fn: Is RemovedStatus: Superseded by ADR-277 — its D1-D4 are D1, D3, D4, D5 there
Supersedes: ADR-013 D5’s “fn: does not chain”, which recorded
the problem as a limitation to live with
Related: ADR-011 D2 (the lowering is name for name),
ADR-018 (a handler is a lambda)
Nikaia had two ways to write a lambda. Part I 5.2 gave fn: expression for one line and 5.3
gave fn { … } for several, and the trailing form — outside the parentheses, as the last
argument — was available to both.
The short form is four characters shorter than the block:
xs.map fn: a.id
xs.map fn { a.id }
and it costs three things.
It does not grow. A body that gains a second line has to change form, not gain a line. That
is not hypothetical: ADR-013 D7 lists three lines of 1brc.nika rewritten from fn: to
fn { … }, and the file still carries the comment saying so. A shorthand that is abandoned the
first time the code changes has not saved anything; it has moved the work later, to the person
least expecting it.
It is a second way to write the same thing. Both forms produce the same closure. The difference is a delimiter, so every reader has to know which to reach for, and ADR-013 D5 had already spent a paragraph on the answer.
Its body swallows the chain. This is the one that matters. trailing_lambda bound its body
to expr, and an expression is greedy over .method():
Server::new()
.route fn: handler(db)
.listen(":8080")
lowered to
Server::new().route(|| { handler(db).listen(":8080") });
The .listen landed inside the lambda. No error, a different program. Both readings are
valid expressions and nothing in the grammar can tell them apart, because there is no token
between the body and the rest of the chain — which is exactly what a block’s } is.
The specification’s own examples were written in the form that misparses: Part II 12.6 chains
.map fn: … into .filter fn: … into .collect(), and Part III 17.1 chains .route("/") fn: …
into .listen(":8080").
fn: expression is removed. There is one lambda form.fn { … } for an implicit-argument lambda, fn(name) { … } where the arguments are named. The
block is the body whether it holds one line or several.
The lambda may go outside the parentheses as the last argument, and where there are no other arguments the parentheses go with it:
let ids = users.map fn { a.id }
let sum = numbers.reduce(0) fn { a + b }
This is not the redundancy D1 removes. fn: and fn { … } differed only in a delimiter;
inside and outside the parentheses differ in where the argument goes, and the outside form is
what lets a chain read down the page.
.route("/x") fn { handler(db) } had no grammar at all: there was a rule for a method with
arguments and a rule for a method with a trailing lambda, and none for both. It is added here,
and it is unambiguous because of D1 — a block ends at its }, so the chain that follows is the
outer chain:
Server::new()
.route("/x") fn { handler(db) }
.listen(":8080")
fn: is an error that says what happened.The form was in the specification and in three examples; somebody will have it in their fingers. A parse error at the colon would be true and useless:
the `fn: …` form was removed (ADR-022): write `fn { … }`. Its body ran to the end of
the expression, so a `.method()` after it landed *inside* the lambda - silently
The silent misparse is gone, and not merely forbidden: with no expression-bodied lambda there is nothing left in the grammar that can swallow a chain.
examples/fortunes.nika parses in full. Its main was the last thing in the repository that
needed the form D3 adds. What that file still waits on is the postgres DSL — a
deferred-parameter block (ADR-007 D4) — and the runtime binding of G6; the syntax is no longer in
the way.
Four code sites and one specification section changed. Part I 5.2 was Expression Lambdas
(The fn: Shorthand); it now records the removal, and 5.3 gains the trailing rule. Part II’s
par_iter chain and Part III’s server example are written in the form that works.
fn: body that ends at the end of the line. It makes both readings decidable and is what
Kotlin and Ruby effectively do, but it makes Nikaia newline-sensitive in exactly one place —
a rule the whole language would have to be read against, bought for four characters.rows.sort_by fn: a.message.cmp(b.message) is the form’s best case. There is no restriction
that keeps that and refuses .listen(":8080").fn: and forbidding a chain after it. It removes the misparse and keeps everything
else the form costs — two ways to say one thing, a body that cannot grow, and now a rule about
where it may appear..route("/x", fn { … })). Unambiguous, works
today, and available to anyone who prefers it — but a chain of trailing lambdas is the shape
Part II 12.6 and Part III 17.1 are written in, and it reads better than the alternative.