Dive C2.07.1
LINQ operators → Seq operators
The same operators by another name, threaded through a pipe instead of a chain.
A LINQ query is a sequence of pure transforms: Where keeps elements,
Select maps them, Aggregate folds them to one value. F# carries
every one of these as a Seq. function, because the type they run on —
IEnumerable<T> — is F#'s seq<'T>. The
visible change is the shape: a C# chain hangs each call off the receiver, while an F# pipeline puts the data first
and lets |> thread it down through each operator. Same operations, same order, read top
to bottom. This dive walks the operator map, worked the same example in both languages.
One operator, two names
The table is the whole dive in one view. Each LINQ method names an operation; each
Seq. function names the same operation. The argument order differs — a LINQ method takes
its lambda and runs on the receiver, while a Seq. function takes its lambda first and the
sequence last, which is what lets |> supply the sequence. Learn the column on the
right and the LINQ knowledge transfers whole.
| LINQ (C#) | F# (Seq.) | what it does |
|---|---|---|
| Where | Seq.filter | keep elements that satisfy a predicate |
| Select | Seq.map | transform each element |
| Aggregate | Seq.fold | fold to a single accumulated value |
| Sum | Seq.sum / Seq.sumBy | add the elements (or a projection of them) |
| GroupBy | Seq.groupBy | group elements by a key |
| First | Seq.head | take the first element |
| Any | Seq.exists | does any element satisfy a predicate |
| ToList | Seq.toList | enumerate and materialise the result |
- seq<'T>
- F#'s name for
IEnumerable<T>— the same .NET interface, the same lazy sequence. - |>
- the pipe operator:
x |> fisf x, so the subject leads and reads left to right. - Seq.toList
- a force point — it enumerates the lazy sequence and builds a concrete
list.
The C# the reader knows, beside the F#
The classic three-step query: keep the even numbers, square them, materialise the result. In C# the calls hang
off the array; in F# the sequence leads and pipes through each operator. The results are identical, and the
echoes were produced — the F# by dotnet fsi, the C# by
dotnet-script.
C# — a method chain off the receiver
var result = new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 } .Where(x => x % 2 == 0) .Select(x => x * x) .ToList(); // [4, 16, 36, 64, 100]
F# — a pipeline; the subject leads
let result = seq { 1..10 } |> Seq.filter (fun x -> x % 2 = 0) |> Seq.map (fun x -> x * x) |> Seq.toList // val result : int list = [4; 16; 36; 64; 100]
The other operators read the same way. Seq.fold is LINQ's
Aggregate — a seed and a combining function. Seq.sumBy sums a
projection in one pass. Seq.groupBy partitions by a key, returning a sequence of
key-and-group pairs.
// Aggregate -> Seq.fold (seed 0, then +) Seq.fold (+) 0 (seq { 1..5 }) // val it : int = 15 // Sum of a projection -> Seq.sumBy [1..6] |> Seq.sumBy (fun x -> x * x) // val it : int = 91 // GroupBy -> Seq.groupBy (key, then the group) seq { 1..6 } |> Seq.groupBy (fun x -> x % 2) |> Seq.map (fun (k, vs) -> k, Seq.toList vs) |> Seq.toList // val it : (int * int list) list = [(1, [1; 3; 5]); (0, [2; 4; 6])]
Seq.filter takes the
predicate first and the sequence last, so data |> Seq.filter pred places
data in that last slot. Every Seq. function is shaped this way,
which is why a pipeline of them reads as one left-to-right flow.Translate a LINQ chain
Pick a LINQ chain over the fixed array. The readout shows the equivalent F# seq
pipeline, operator for operator, and the identical result — the same value whether you read the C# or the F#.
The diagram shows the operators as stages the data flows through, left to right.
LINQ chain → Seq pipeline · same result
F# · seq { 1..10 } |> Seq.filter (fun x -> x % 2 = 0) |> Seq.map (fun x -> x * x) |> Seq.toList = [4; 16; 36; 64; 100]
| LINQ (C#) | F# (Seq.) | result |
|---|---|---|
| .Where(x=>x%2==0).Select(x=>x*x) | Seq.filter … |> Seq.map … | [4; 16; 36; 64; 100] |
| .Aggregate(0,(a,x)=>a+x) over 1..5 | Seq.fold (+) 0 | 15 |
| .Sum(x=>x*x) over 1..6 | Seq.sumBy (fun x -> x*x) | 91 |
| .GroupBy(x=>x%2) over 1..6 | Seq.groupBy (fun x -> x%2) | [(1, [1; 3; 5]); (0, [2; 4; 6])] |
The translation is mechanical because the operations are the same. A C# developer reading the right column is reading a query they already know, with the receiver moved to the front and the method names respelled.
Why the pipe, not the chain
A C# chain works because each method returns an IEnumerable<T> the next can hang
off. An F# pipeline works because each Seq. function takes the sequence as its last
argument, so |> can feed the previous result into that slot. Both read as one flow;
the pipe makes the threading an operator rather than a dot. The gain is uniformity —
|> works on any value, not only ones that expose fluent methods, so the same pipeline
shape covers list, array, and seq
alike.
C# habit
A LINQ method chain — data.Where(…).Select(…).ToList() — each call hung off the value the last returned.
F# form
A |> pipeline — data |> Seq.filter … |> Seq.map … |> Seq.toList — the subject threaded through each operator.
Where this leads
The operator names are the easy half. The half a C# developer must hold onto is that
seq, like LINQ, does no work until something enumerates it — the pipeline above
is a recipe, not a run, until Seq.toList forces it. That deferral is the next dive, and
it is what lets a seq compose even over an infinite source.
References
Sources
- Microsoft — F# documentation — sequences and the operator-by-operator
Seqguidance. - FSharp.Core API reference — the
Seqmodule:filter,map,fold,sumBy,groupBy. - Microsoft — .NET documentation — LINQ standard query operators and
IEnumerable<T>.
Related in this course
- C2.07.2 — Deferred execution & laziness — why the pipeline runs only when forced.
- C1.07 — Lists, arrays & the lazy seq — the transform operators in language depth.
- C2 — F# for C# developers — each C# habit, translated to its idiomatic F# form.