7.1.56. C#-style LINQ query syntax (%linq!)

This tutorial covers daslib/linq_das — the %linq! ... %% reader macro that adds a C#-like query-expression form on top of the _fold engine. A query is a purely compile-time rewrite into a fused _fold(...) chain, so it costs nothing over the hand-written pipe form, and the same query runs over arrays, decs, SQL, and XML.

It assumes you have read LINQ — Language-Integrated Query (the pipe-form linq surface and the _fold macro). The full clause grammar and per-source details live in the C#-style LINQ query syntax (%linq!) reference page.

7.1.56.1. from / where / select

The range variable (c) is spliced verbatim as the lambda parameter. select c (the variable alone) is the identity projection; any other projection emits a _select.

let names <- %linq! from c in cars where c.price > 100 select c.name %%
print("expensive: {names}\n")
// output: expensive: [ m3, a4, a8]

7.1.56.2. orderby (multi-key)

Comma-separated keys, each with its own optional ascending / descending. More than one key emits a single composite stable sort.

let sorted <- %linq! from c in cars orderby c.brand, c.price descending
                     select (b = c.brand, p = c.price) %%
// audi 400, audi 200, bmw 250, bmw 100, kia 50

7.1.56.3. group … by (IGrouping)

A bare group c by k yields one (key, [members]) tuple per bucket. Read the key as ._0 and the members as ._1.

let buckets <- %linq! from c in cars group c by c.brand %%
for (g in buckets) {
    print("{g._0}: {g._1 |> length} cars\n")
}

7.1.56.4. group … into g … select (grouped aggregation)

into g rebinds the group to a new range variable and continues the query. With the A2 convention g.key is the bucket key and a bare g is the member collection, so the familiar LINQ aggregates read naturally. This is the canonical C# grouped-aggregation idiom, and over a SQL source it pushes straight down to GROUP BY + COUNT/SUM/AVG/....

let report <- %linq! from c in cars
                     group c by c.brand into g
                     select (brand = g.key,
                             count = g |> length,
                             total = g |> select($(u : Car) => u.price) |> sum) %%
// audi: count=2 total=600 ; kia: count=1 total=50 ; bmw: count=2 total=350

The continuation may itself filter / order the groups:

let bigBrands <- %linq! from c in cars
                        group c by c.brand into g
                        where g |> length > 1
                        orderby g |> select($(u : Car) => u.price) |> sum descending
                        select g.key %%
// output: [ audi, bmw]

7.1.56.5. select … into n (projection continuation)

select <proj> into n rebinds the projected value. Continuations chain, so a query can project, filter, then re-order, all fused on one pass with no intermediate array.

let dear <- %linq! from c in cars
                   select c.price into p
                   where p >= 200
                   orderby p descending
                   select p %%
// output: [ 400, 250, 200]

7.1.56.6. let bindings and join

let introduces a computed value reused downstream (inlined at compile time). join ... on ... equals ... is a single inner equi-join introducing a second range variable.

let located <- %linq! from c in cars join h in hqs on c.brand equals h.brand
                      select (car = c.name, country = h.country) %%

7.1.56.7. group join (join … into g)

join ... equals ... into g is C# GroupJoin: g binds the array of matching right rows alongside the left variable, and the terminal select reads both. It is outer – every left row surfaces, an unmatched one with an empty group. Below, the cars are grouped under each brand HQ; "tesla" has no cars, so it still appears with count 0.

let perHq <- %linq! from h in hqs2
                    join c in cars on h.brand equals c.brand into g
                    select (brand = h.brand,
                            country = h.country,
                            n     = g |> length,
                            total = g |> select($(u : Car) => u.price) |> sum) %%
// output (one row per HQ; tesla has n=0 total=0)

It is array-source, select-terminal only (a pre-join where is allowed); over a SQL source the group join is in-memory only. See Join for the exact scope.

See also

C#-style LINQ query syntax (%linq!) — the full %linq! clause grammar, the four sources, and the aggregate-vs-member-keeping rules for into over SQL.

LINQ-fold patterns — what _fold(...) recognizes — catalog of chain shapes that _fold recognizes, and the splice arm each one fires.

LINQ — Language-Integrated Query — the underlying pipe-form linq surface and the _fold macro.

Full source: tutorials/language/56_linq_query.das

Previous tutorial: LINQ over DECS