ADR-022: One Lambda Form. fn: Is Removed

Status: 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)


1. Context

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").


2. Decisions

D1 — 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.

D2 — A trailing lambda stays, and it is what makes a chain readable.

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.

D3 — A method may take arguments and a trailing lambda.

.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")

D4 — Writing 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

3. Consequences

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.


4. What was rejected

This site is open source. Improve this page.