7.1.28. LINQ — Language-Integrated Query

This tutorial covers daslang’s LINQ module for C#-style query operations on iterators and arrays, plus the linq_boost macros for composing pipelines.

7.1.28.1. Setup

Import both modules:

require daslib/linq
require daslib/linq_boost

Most LINQ functions come in several flavors:

  • func(iterator, ...) — returns a lazy iterator

  • func(array, ...) — returns a new array

  • func_to_array(iterator, ...) — materializes into an array

  • func_inplace(var array, ...) — mutates in place

7.1.28.2. Filtering

where_ filters elements by a predicate (the trailing underscore avoids collision with the built-in keyword):

var numbers = [3, 1, 4, 1, 5, 9, 2, 6, 5]
var evens = where_to_array(numbers.to_sequence(), $(x) => x % 2 == 0)
// evens: [4, 2, 6]

The _where shorthand uses _ as the element placeholder:

var big = _where_to_array(numbers.to_sequence(), _ > 4)
// big: [5, 9, 6, 5]

7.1.28.3. Projection

select transforms each element:

var doubled = _select_to_array(numbers.to_sequence(), _ * 2)
// doubled: [6, 2, 8, 2, 10, 18, 4, 12, 10]

select_many flattens nested sequences into one:

var nested = [
    ["a", "b", "c"].to_sequence(),
    ["d", "e", "f"].to_sequence()
]
var flat = select_many_to_array(
    nested.to_sequence(),
    $(s : string) => "{s}!"
)
// flat: ["a!", "b!", "c!", "d!", "e!", "f!"]

7.1.28.4. Sorting

order sorts ascending, order_descending sorts descending:

var unsorted = [5, 3, 8, 1, 4]
var asc = order(unsorted)           // [1, 3, 4, 5, 8]
var desc = order_descending(unsorted) // [8, 5, 4, 3, 1]

Sort structs by a field with _order_by:

var by_age = _order_by(team.to_sequence(), _.age)

7.1.28.5. Partitioning

  • skip(arr, n) / take(arr, n) — from the front

  • skip_last(arr, n) / take_last(arr, n) — from the end

  • skip_while / take_while — by predicate

  • chunk(iter, n) — split into groups of N

var seq = [for (x in 0..8); x]
skip(seq, 3)        // [3, 4, 5, 6, 7]
take(seq, 3)        // [0, 1, 2]
take_last(seq, 3)   // [5, 6, 7]
skip_last(seq, 3)   // [0, 1, 2, 3, 4]

7.1.28.6. Aggregation

Function

Description

count(arr)

Number of elements

count(arr, pred)

Number of elements matching predicate

sum(iter)

Sum of elements

average(iter)

Arithmetic mean (always returns double)

min(iter) / max(iter)

Minimum / maximum

aggregate(iter, seed, func)

Custom fold with seed

var vals = [10, 20, 30, 40, 50]
count(vals)                          // 5
count(vals, $(v) => v > 25)          // 3
sum(vals.to_sequence())              // 150
aggregate(vals.to_sequence(), 1, $(acc, v) => acc * v)  // 12000000

The _count shorthand works like _where:

_count(vals.to_sequence(), _ >= 30)  // 3

7.1.28.7. Element access

  • first / last — first or last element (panics if empty)

  • first_or_default / last_or_default — returns default if empty

  • element_at(iter, i) / element_at_or_default(iter, i)

  • single — returns the only element (panics if count ≠ 1)

7.1.28.8. Querying

any(vals.to_sequence(), $(v) => v > 40)   // true
all(vals.to_sequence(), $(v) => v > 5)    // true
contains(vals.to_sequence(), 30)           // true

7.1.28.9. Set operations

var a = [1, 2, 2, 3, 3, 3]
var b = [2, 3, 4, 5]
distinct(a)     // [1, 2, 3]
union(a, b)     // [1, 2, 3, 4, 5]
except(a, b)    // [1]
intersect(a, b) // [2, 3]

7.1.28.10. Zip

zip merges 2 to 8 sequences into tuples; iteration stops at the shortest source:

var names = ["Alice", "Bob", "Charlie"]
var ages  = [25, 35, 30]
var zipped = zip(names, ages)
// zipped: [(Alice,25), (Bob,35), (Charlie,30)]

var scores = [95, 87, 91]
var zipped3 = zip(names, ages, scores)
// zipped3: [(Alice,25,95), (Bob,35,87), (Charlie,30,91)]

Each arity (2..8) has four overloads: lockstep and result-selector forms, each on iterator and array sources. A result-selector block runs per element and returns a custom projection (the tuple is replaced by whatever the block returns):

var sums = zip(a, b, c, d, $(p, q, r, s : int) => p + q + r + s)

Add _to_array to force materialization of an iterator-source zip into an array:

var arr = zip_to_array(seqA, seqB, seqC)

7.1.28.11. Joining

Five join shapes share most of the surface; only the into lambda’s argument Option-ness differs per kind. _cross_join is the one exception — 3-arg form, no on lambda.

Operator

into lambda signature

Cardinality

_join

(a : TA, b : TB)

matched pairs only

_left_join

(a : TA, b : Option<TB>)

every TA, optional TB

_right_join

(a : Option<TA>, b : TB)

every TB, optional TA

_full_outer_join

(a : Option<TA>, b : Option<TB>)

every TA + every TB

_cross_join

(a : TA, b : TB)

Cartesian (TA × TB)

The on lambda must be an equi-join: $(a, b) => a.K == b.K. Theta joins and &&-chained multi-key equality are rejected at macro time:

let ids     = [1, 2, 3, 4, 5]
let matches = [2, 3, 3, 4]   // 3 has two matches; 1 and 5 are unmatched

// Inner — matched pairs only.
var inner = _join(ids, matches,
    $(la : int, lb : int) => la == lb,
    $(la : int, lb : int) => (la, lb))
// [(2,2), (3,3), (3,3), (4,4)]

// Left — every left row; right is Option.
var lj = _left_join(ids, matches,
    $(la : int, lb : int) => la == lb,
    $(la : int, lb : Option<int>) => (la, lb |> unwrap_or(-1)))
// [(1,-1), (2,2), (3,3), (3,3), (4,4), (5,-1)]

// Right — every right row; left is Option.
var rj = _right_join(ids, matches,
    $(la : int, lb : int) => la == lb,
    $(la : Option<int>, lb : int) => (la |> unwrap_or(-1), lb))
// [(2,2), (3,3), (3,3), (4,4)]

// Full outer — both sides surface; either may be none.
var fj = _full_outer_join(ids, matches,
    $(la : int, lb : int) => la == lb,
    $(la : Option<int>, lb : Option<int>) => (la |> unwrap_or(-1), lb |> unwrap_or(-1)))
// [(1,-1), (2,2), (3,3), (3,3), (4,4), (5,-1)]

// Cross — Cartesian, no `on` lambda.
var cj = _cross_join([1, 2], ["a", "b", "c"],
    $(la : int, lb : string) => (la, lb))
// [(1,a), (1,b), (1,c), (2,a), (2,b), (2,c)]

The function form left_join_to_array (and matching join_to_array / right_join_to_array / full_outer_join_to_array / cross_join_to_array) takes explicit per-side key selectors and materializes into an array.

Under _sql(...) from dasSQLITE, all five join shapes lower to native SQL — see SQL-16 — All Join Shapes for the details and the is_some / is_none projection idiom.

7.1.28.12. The _fold pipeline macro

_fold composes multiple LINQ operations into an optimized pipeline using dot-chaining:

// Sum of squares of even numbers
var result = (
    [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    ._where(_ % 2 == 0)
    ._select(_ * _)
    .sum()
    ._fold()
)
// result: 220

// Top 3 unique values, descending
var top3 = (
    [5, 3, 8, 1, 4, 8, 3, 9, 2, 9]
    .order_descending()
    .distinct()
    .take(3)
    ._fold()
)
// top3: [9, 8, 5]

The _fold macro rewrites the chain into efficient imperative code at compile time, avoiding intermediate allocations.

See also

Full source: tutorials/language/28_linq.das

Next tutorial: Functional Programming