Issue #623: A parameter may take a tuple apart, in a function and a lambda (ADR-358)

Open and answer this issue on GitHub.

ready · opened 2026-10-14 · updated 2026-10-28 · 3 comments

What this is about, from zero

A lambda is a function written in place: xs.iter().filter(fn(x) { x > 1 }). A walk (filter, map, any, …) hands its lambda one item at a time. When the items are tuples - a Vec[(String, i64)], a map’s pairs - that one item is a pair:

let pairs: Vec[(String, i64)] = [("a".clone(), 1), ("b".clone(), 2)]
let n = pairs.iter().filter(fn(pair) { let (name, v) = pair
    v > 1 }).count()

Authors (and rs2nika’s drafts) write the shorter form, naming the parts as parameters:

let n = pairs.iter().filter(fn(name, v) { v > 1 }).count()

Today (0.0.952) the checker refuses that form with NK1101: This lambda takes 2 arguments, but it is handed one tuple, and the help writes the one-argument form with let (name, v) = pair. Before 0.0.952 it reached rustc, which refused |name, v| with E0593 (lowering-defects D18). So nothing is broken now; the question is whether the short form should mean the destructuring.

Where the question comes from

docs/lowering-defects.md D18, met when rs2nika drafts of check/mod.rs were run through lower-std (2026-10-11). It is a language question, not a defect: no ADR, spec section or issue decides it (searched: ADR-231 decides how filter’s lambda takes its item, not tuples; Part I’s lambda section names no pattern parameters). Nothing needs the answer now: the refusal names the spelling that works. It is a convenience question.

How other languages do it

Language Two names for one pair Spelling
Rust refused (E0593) \|(name, v)\| - a pattern in the parameter
Swift accepted until Swift 4 (SE-0110 removed the implicit splat, then a special case brought it back for closures) { (name, v) in }
Kotlin refused for Pair { (name, v) -> } - parentheses mark destructuring, distinct from { name, v -> }
JavaScript refused in effect (second parameter is the index) ([name, v]) => ...
Python refused since Python 3 (PEP 3113 removed tuple parameters) lambda pair: ...

Every one of them that allows it marks the destructuring with brackets; none lets the count of parameters decide silently, because fn(a, b) also means “two arguments” for fold and for function values.

Options

  1. Keep the refusal (today). One spelling, let (a, b) = pair in the body. Cost: one line per lambda.
  2. A pattern in the parameter, with brackets: fn((name, v)) { .. }, lowering to Rust’s |(name, v)|. Explicit, matches Rust and Kotlin; needs a grammar change (a parameter becomes a pattern) and the checker must type it.
  3. Splat by count: fn(name, v) takes a pair apart when the place hands one tuple of that size. Shortest; but fn(a, b) then means two things depending on the item type, and a change of the item type silently changes the meaning (Swift’s SE-0110 story).

Risk if decided wrong

Option 3 makes a program’s meaning depend on an inferred type; a refactor of the item type turns a destructuring into an arity error or the reverse. Option 2 adds grammar for a convenience. Option 1 costs nothing but a line.

Recommendation

Option 1 now, option 2 if a later round wants it: the refusal with its help is already built (0.0.952) and teaches the working form; bracketed pattern parameters are the only form other languages converged on and can be added later without breaking anything. Not option 3.

To build (only if option 2 is chosen)

  1. Grammar: a lambda parameter may be a tuple pattern (a, b).
  2. Checker: the pattern’s names are typed from the tuple the place hands; a size mismatch is NK1101.
  3. Lowering: |(a, b)|.
  4. Tests: filter, map over pairs, a map’s iter(); the refusal of fn(a, b) stays.

Comments

keywan-ghadami, 2026-10-15

Decision (owner, 2026-10-15): option 2 for functions and lambdas alike

How it went. The issue recommended option 1 now and option 2 for lambdas later. Two points came up with the owner.

Checked on 0.0.966:

Form Result
fn(name, v) NK1101
fn((name, v)) parse error
fn(pair) { let (name, v) = pair … }, the form NK1101’s help recommends refused by rustc (v is a reference). Filed as #626

Decision. Recorded in ADR-358 (D1-D5), Part I 2.1 (Taking a tuple apart) and 5.3, and Part III 13.5 and C.3. Version 0.0.967, commit 4069587.

  1. A positional parameter of a function or a lambda may be one irrefutable tuple pattern, ADR-356’s: fn dist((x, y): (f64, f64)), fn((name, v)). A function writes the whole type after it. An option is never a pattern, and a variant is NK1259 (D1).
  2. It stays one parameter for callers, the function’s type and arity. fn(a, b) stays two arguments. NK1101’s help now writes fn((name, ty)) (D2).
  3. ref and mut belong to the whole parameter: the parts become views or changeable (D3).
  4. Ledger: signature writes the pattern as written, and every other key names the parts by name. A caller maps a part to its argument’s component (D4).
  5. Lowering: a hidden parameter name plus a let at the top of the body (D5).
  6. Rejected:
    • refusing it;
    • lambdas only;
    • splitting by the number of names.

To build

  1. Parser. A parameter of fn (with : Type) and of a lambda may be a tuple pattern. (p) is p. A variant there is NK1259, and an option written as a pattern is refused.
  2. Checker. Bind the parts with ADR-356’s walk, from the declared type (function) or from the item the place hands (lambda). Keep NK1101 for fn(a, b) over one tuple, with the new help.
  3. Lowering. A hidden parameter name and a let taking it apart at the top of the body. Parts of a borrowed tuple are read as the for head reads them (this is the same fix as #626).
  4. Ledger. Write the pattern in signature. Every key that names a parameter (keeps, returns, from(p), code, maps, views, touches, sharing) names parts. Readers map a part to the argument’s component.
  5. Tests:
    • filter(fn((name, v)) { v > 1 }) over Vec[(String, i64)] prints 1;
    • map(fn((k, v)) { … }) over a map’s iter();
    • a nested pattern fn((a, (b, c)));
    • fn dist((x, y): (f64, f64)) called with a tuple;
    • fn pick(ref (a, b): (String, String)) -> ref String returning a or b, with the ledger writing ref(a | b) and a caller in another package reading it;
    • a mut (a, b) parameter whose change reaches the caller;
    • fn(Some(x)) is NK1259;
    • fn(a, b) over pairs is still NK1101, with the new help.

The commit that builds the last step says Fixes #623 (and #626 if it lands with step 3).


Generated by Claude Code

keywan-ghadami, 2026-10-27

Met again in rs2nika (#877, R110-R113): a lambda over a Seq of pairs cannot take the pair apart. fn(k, v) is refused with NK1101, and fn((k, v)) does not parse. The converter turns Rust’s .map(|(k, v)| ..) over zip/map entries into fn(k, v), which is not valid Nikaia. Those lines stay wrong until this issue is built.


Generated by Claude Code

keywan-ghadami, 2026-10-28

Built in 0.0.1262 (5c14b5f): the lambda half. fn((a, b)) { … } and nested fn(((a, b), t)) parse. The pattern becomes one parameter under a hidden __nikaia_tuple<byte> name, and a let at the top of the body takes it apart. A pattern of the wrong width is NK1283 (“A lambda’s parameter takes N parts apart…”). NK1101’s help now writes the pattern out.

Still open: the pattern in a function’s parameter (fn dist((x, y): (f64, f64))), its ledger keys, ref/mut before a pattern (D3, D4), and fn(Some(x)) as NK1259.


Generated by Claude Code

Back to Open Issues