# Daf.Linq — Lazy Sequences for Delphi
🌍 Language: English | Español
A standalone, ref-counted, lazy collection-transformation library — LINQ-to-Objects / Kotlin sequences / Rust iterators in spirit. It knows nothing about SQL and is useful on its own.
Concepts
| Concept | Description |
|---|---|
TQuery<T> |
The fluent operator surface: a value type wrapping a deferred System.IEnumerable<T>, converting implicitly to and from it |
IGrouping<TKey, T> |
A group produced by GroupBy: a Key plus its Members |
Linq.From<T> |
Entry point — builds a TQuery<T> over an array, a TEnumerable<T>, an IEnumerable<T> or a TSmartArray<T> |
Linq.FromOwned<T> |
Entry point over a temporary list that Linq frees with the query — see Ownership |
Delphi interfaces cannot declare generic methods, which is why the generic operators (Select, Map, Zip, GroupBy) live on the TQuery<T> record rather than on the interface. Pass IEnumerable<T> around; chain on TQuery<T>.
Quick Start
uses Daf.Linq;
var Adults := Linq.From<TPerson>(People) // People: TList<TPerson>
.Where(function(P: TPerson): Boolean begin Result := P.Age >= 18 end)
.OrderBy(TComparer<TPerson>.Construct(
function(const A, B: TPerson): Integer begin Result := CompareStr(A.Name, B.Name) end))
.Take(10);
for var P in Adults do // nothing ran until this line
Writeln(P.Name);Adults is an IEnumerable<TPerson> — deferred, ref-counted, no Free.
Operators
| Operator | Kind | Behavior |
|---|---|---|
Where / Filter |
lazy | Keeps elements matching a TFilterSpec<T> (a bare closure converts implicitly) |
Select<TResult> / Map<TResult> |
lazy | Projects each element through a TFunc<T, TResult> |
Take(N) |
lazy | Yields at most N, then stops consuming the source |
Skip(N) |
lazy | Drops the first N |
Zip<TOther, TResult> |
lazy | Pairs with another sequence element-wise until either ends |
Distinct(Comparer) |
buffered | De-duplicates by value; pass a NULL-aware comparer for SQL semantics |
OrderBy(Comparer) |
buffered | Sorts by an IComparer<T>, and is stable, as in .NET: elements the comparer considers equal keep their source order. Descending is an inverted comparer: there is no OrderByDescending |
GroupBy<TKey>(KeySelector) |
buffered | Yields IEnumerable<IGrouping<TKey, T>>, key order preserved |
IsEmpty |
terminal | True when nothing is produced; stops at the first element |
Any(Spec) |
terminal | True when some element matches; stops at the first match |
FirstOrDefault / LastOrDefault |
terminal | The first / last element, or Default(T) (or a supplied fallback) |
TryFirst(out Value) |
terminal | The first element plus a found/not-found result |
Single / SingleOrDefault / TrySingle |
terminal | The one and only element — see below |
ToArray / ToList / ToList(List) / ToSmartArray |
terminal | Materializes the sequence |
MoveToList(List) / MoveToList / MoveToArray |
terminal | Moves the elements: takes them out of the source list as it delivers them — see Ownership |
Deferred by default. Where/Select/Take/Skip/Zip run element-by-element while iterating. OrderBy/Distinct/GroupBy need every element, so they materialize once when enumeration starts — but still nothing happens until then.
Terminals are non-destructive. They enumerate without consuming the query, so the same value stays usable and a terminal can be called twice:
var Q := Linq.From<TPerson>(People).Where(IsAdult);
if not Q.IsEmpty then
for var P in Q do ... // still yields everythingThe flip side is that each terminal call re-enumerates the source. With an expensive or single-shot source, materialize once (ToArray / ToList) and query that.
The exceptions are in Ownership: over a source Linq owns, an element a terminal hands to the application is not yielded again, and the MoveTo… terminals take what they deliver out of the source list.
First… or Single…?
Single asserts a shape: it raises ELinqError when the sequence is empty or holds more than one element. SingleOrDefault tolerates empty but still raises on more than one. Use them when “more than one” means your data is wrong and you want to hear about it:
Users.Where(EmailIs(X)).SingleOrDefault; // a duplicate email is a bug → raise
Entries.OrderBy(NewestFirst).FirstOrDefault; // many entries expected → take the newestTryFirst / TrySingle are the non-raising forms. TryFirst is also the only way to tell an empty sequence from one whose first element happens to equal Default(T) — something FirstOrDefault cannot express for value types.
Ownership
Linq handles references, never objects: which list frees an object is decided by five rules.
Linq.From(X)— X belongs to the application. Linq never frees it, nor any of its objects.Linq.FromOwned(X)— X is a temporary list that nobody else keeps. Linq frees it when the last query built on it is released.- The operators (
Where,Select,Take…) create nothing: they keep referencing the source. - What a terminal returns belongs to the application. The container it creates, always. The objects, with
From, stay with whoever owned them; withFromOwned, they pass to the application. An object a lambda creates belongs to the application in both cases. MoveToList/MoveToArraytake what they deliver out of the source list, and free nothing. They are the only terminals that change the source.
A source Linq owns
FromOwned takes a TObjectList<T> — or a list derived from it — of a class T. It is the entry for a list that a call returns and nobody keeps in a variable:
Orders := TObjectList<TOrder>.Create; // owns its objects
Linq.FromOwned<TOrder>(LoadOrders(Customer)) // a temporary list
.Where(IsPending)
.ToList(Orders);
// the pending orders now belong to Orders; the temporary list and the
// other orders are freed with the queryWhat each terminal does over an owned source:
| Terminal | Over an owned source |
|---|---|
ToList(List) / ToList / ToArray / ToSmartArray |
Every element put in the container passes to the application |
FirstOrDefault / LastOrDefault / TryFirst / Single / SingleOrDefault / TrySingle |
The element returned passes to the application; a fallback value passed in is not the source’s and is not touched |
IsEmpty / Any |
No element changes owner |
for..in |
Lends the elements while the query lives |
- An element that passed to the application is not yielded again by any later terminal over the same source.
- When the list does not own its objects (
OwnsObjects = False), Linq frees only the list, and no element changes owner. - When adding an element to the target list raises, the element stays where it ended up — in the target if the target kept it, in the source otherwise — and the exception reaches the caller.
- To look at an object without taking it, project before the terminal:
Linq.FromOwned<TOrder>(LoadOrders(Customer)).Select<string>(OrderNumber).FirstOrDefaultreturns a string, and every order stays with the source. - The query’s temporaries live until the end of the routine that builds it, or inside a loop until the next iteration, and the source lives with them. To release it earlier, keep the query in a variable and reset it:
Q := Default(TQuery<TOrder>).
Limits:
- Converting the query to
IEnumerable<T>— implicitly or withAsEnumerable— andGroupBylend the elements: what is read through them is valid while the query lives, and no terminal reached that way hands anything over. - A projection that returns an object owned by an element — a
SelectreturningOrder.Customer— does not change its owner: it lives as long as the element. - A list the application still uses is freed with the query:
FromOwnedis only for a list nobody else keeps.
Moving elements to another list
When the application keeps the source and wants part of it in another list, MoveToList(List) takes each selected element out of the source list — with Extract, which does not free it — and adds it to List. MoveToList without arguments does the same into a new list, and MoveToArray into a new array.
Linq.From<TOrder>(Orders) // Orders stays with the application
.Where(IsArchived)
.MoveToList(Archived);
// the archived orders left Orders and are in Archived: one owner each- Moving needs a query that reaches a list:
Linq.FromorLinq.FromOwnedover aTList<T>, through operators that keep the element type (Where,Take,Skip,Distinct,OrderBy). Over an array or anIEnumerable<T>, or afterSelectorZip, the three raiseELinqError(MoveToList: the query does not reach a source list…) and the source is left unchanged. - The selection is collected before anything moves, so the order of the chain is the order in the target.
- When adding an element to the target raises, the element stays where it ended up, as with an owned source.
- Over a source Linq owns, what is moved leaves the list and is not freed with it.
One vocabulary, two execution strategies
Commons ships an eager collection (TSmartArray<T>) that predates this module. Both speak the same filter and mapper types, so a value written once works with either:
| Concern | Shared type | Eager (TSmartArray<T>) |
Lazy (TQuery<T>) |
|---|---|---|---|
| filter | TFilterSpec<T> |
Where, Any |
Where / Filter, Any |
| projection | TFunc<T, U> |
Select<U> |
Select<U> / Map<U> |
| eager → lazy | — | Linq.From<T>(SmartArray) |
|
| lazy → eager | — | ToSmartArray |
var Adults: TFilterSpec<TPerson> := ...;
var Names: TFunc<TPerson, string> := ...;
Eager.Where(Adults).Select<string>(Names); // eager, Commons
Linq.From<TPerson>(People).Where(Adults).Select<string>(Names); // lazy, hereDeliberately different on each side:
SortvsOrderBy— the eagerSortmutates in place;OrderByreturns a new deferred sequence. Different semantics, different names.- Lazy only —
Take,Skip,Zip,GroupBy,Distinctand the terminals above. - Eager only —
Concat,BinarySearch,FreeItems,First/Lastproperties: container concerns, not sequence ones.
Why both exist. Commons’ own Daf.Rtti chains Where(...).Select(...) over a TSmartArray and sits below this module in the dependency graph, so it cannot use the lazy API without inverting that graph. The duplication of implementation is therefore structural, not accidental — but the vocabulary is not duplicated.
Common Patterns
Group and summarize
for var G in Linq.From<TPerson>(People).GroupBy<string>(
function(P: TPerson): string begin Result := P.Country end) do
Writeln(Format('%s: %d', [G.Key, Length(G.Members)]));Bridge to and from the eager TSmartArray<T>
Commons already ships an eager, array-backed collection. Daf.Linq reuses its vocabulary rather than inventing a second one, so eager and lazy are two execution strategies over one API:
uses Daf.Arrays, Daf.Linq;
var Lazy := Linq.From<TPerson>(Eager); // TSmartArray<T> → lazy
var Eager2 := Lazy.Where(IsAdult).ToSmartArray; // lazy → TSmartArray<T>Short-circuiting a large source
Linq.From<Integer>(Million).Where(IsPrime).Take(5).ToArray;
// stops scanning as soon as the fifth prime is producedNotes
Reseton the underlying enumerators is not supported — sequences are forward-only.- Value-type
T(records,TNullable<T>) is supported: theDaf.Enumerableadapters resolve the non-genericGetCurrent: TObjectbaggage through a method-resolution clause. - For NULL-aware ordering and de-duplication, use the comparers in Commons’
Daf.Comparers(TValueComparers,TNullableComparers).
Documentation
| Resource | Description |
|---|---|
| SQLCute In-Memory | Runs a SQLCute IQuery over in-memory collections on top of this library |
| In-Memory Usage Guide | Two lanes, NULL semantics, DISTINCT scope |
| SQLCute | The fluent SQL query builder |