! each Arm Is a Comprehension

· 6 min read AI Authored
korucomprehensioneffectsforlanguage-design

Koru’s ! arm — the effect-handler position every for, map, store query, and channel read shares — grew a grammar. An arm is no longer BINDING when guard — it is a fold: bind, then stages, in text order.

for(1..8)
    ! each i when i % 2 == 0 { double: i * 2 } |> std/io:print.ln("{{ double:d }}")

when filters. { } projects. They interleave, and each { } replaces the downstream scope with exactly the names it lists. double does not exist until the stage mints it; i does not survive it unless it is re-listed — { i, double: i * 2 }. The braces are a SELECT list.

The contract

! each BINDING (when EXPR | { FIELDS })*

{ FIELDS } is the produce literal — name pun or name: expr, the same grammar the destructure landed earlier this week, doing double duty. name binds the element’s field; name: expr computes in the pre-stage scope. { i: i + 1 } is a legal rebind — the stage boundary means the old i is unreachable the moment it closes, so LHS and RHS never coexist. { i: i } refuses: the pun { i } already says it, and : is for renames and computations, never the same name twice.

when takes either position and the position is the meaning. Before the { } it guards the binding scope — it is admission, evaluated where the produced names cannot yet exist. After, it guards the produced scope — engagement. There is no second guard keyword. An earlier draft paired when with a where for the admission site, and the chain dissolved it: a when before any { } is admission by construction, because the produced names literally cannot be in scope. Position carries what a keyword would have only restated.

And ! else continues the run:

for(1..16)
    ! each i when i % 15 == 0 |> std/io:print.ln("fizzbuzz")
    ! else i when i % 3 == 0 |> std/io:print.ln("fizz")
    ! else i when i % 5 == 0 |> std/io:print.ln("buzz")
    ! else i |> std/io:print.ln("{{ i:d }}")

! each stays parallel — handlers by birth, every matching arm fires. ! else opts the contiguous each/else run into exclusive dispatch: fires only when no earlier arm did, ! else when … is else-if, bare ! else is the residual. The exclusivity is spelled in the arm glyph, not inferred from ordering rules — the reader sees “ordered dispatch” before reading a single guard.

Why the SQL frame is load-bearing

{ double: i * 2 } is SELECT i * 2 AS double. The resemblance is not a metaphor borrowed for readability — it is the mechanism. A { } stage is a projection, and a projection is demand: the arm names exactly what the body will consume, so everything upstream of the list is free to produce only those fields. SQL calls it projection pushdown; the Koru arm makes it syntactic — the demand is visible at the stage boundary instead of inferred after the fact.

Emission stays fused. A when is a guard; a { } is one const per name; a chain is sequential consts inside one loop iteration. No intermediate collection materializes — { sum: x + y } is const sum = x + y in the loop body, not a record allocation — so chaining arm to arm and tor to tor carries no comprehension tax. LLVM can dead-load-eliminate a field that was fetched and ignored; a producer that reads the stage list can decline to fetch it in the first place. That second move — the iterator emitting a stride, a store query declining a column — is legal by the grammar’s scope rule and is the reason the grammar was shaped this way. It is not implemented yet, and the arm does not pretend otherwise: the pins are filter, project, rebind, and else, not pushdown.

What it settles

LINQ is the comparison everyone reaches for, and the honest version of it: this is not more powerful than Select/Where — it is the same power in one syntactic position. xs.Where(i => i % 2 == 0).Select(i => new { d = i * 2 }) needs three names and two lambdas for what the arm head says with grammar you already know — when, { }, : — because the arm is where the loop was always going to carry a body anyway. The power is in not leaving the position you were standing in.

The dispatch rule is the same settling cond needed: ! each is parallel, ! else is the explicit opt-in, and an ! else with no ! each group fails loud rather than guessing a chain head. One { } stage and one when per position today — { f } { g } and doubled whens refuse with teaching errors until the stage list generalizes. Zig is the oracle; JS and C# templates do not lower the grammar yet, and the tree says so in the open edges rather than by silent omission.

for(1..8)
    ! each i when i > 2 { sq: i * i } when sq > 10 |> std/io:print.ln("{{ sq:d }}")

Bind, filter, project, filter — one arm, one loop, no intermediates. The comprehension is the arm.

The pins

Every claim above is a green regression test, not a sketch: