jonnifyfsharp

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.

C2.07.1 · part 1

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 method · F# Seq function · what it does
LINQ (C#)F# (Seq.)what it does
WhereSeq.filterkeep elements that satisfy a predicate
SelectSeq.maptransform each element
AggregateSeq.foldfold to a single accumulated value
SumSeq.sum / Seq.sumByadd the elements (or a projection of them)
GroupBySeq.groupBygroup elements by a key
FirstSeq.headtake the first element
AnySeq.existsdoes any element satisfy a predicate
ToListSeq.toListenumerate and materialise the result
seq<'T>
F#'s name for IEnumerable<T> — the same .NET interface, the same lazy sequence.
|>
the pipe operator: x |> f is f 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.
C2.07.1 · part 2

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])]
The argument order is the reason the pipe works. 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

seq { 1..10 } Seq.filter Seq.map [4; 16; 36; 64; 100]
C# · new[]{1..10}.Where(x=>x%2==0).Select(x=>x*x).ToList() = [4, 16, 36, 64, 100]
F# · seq { 1..10 } |> Seq.filter (fun x -> x % 2 = 0) |> Seq.map (fun x -> x * x) |> Seq.toList = [4; 16; 36; 64; 100]
Without JavaScript: the same four chains, side by side
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..5Seq.fold (+) 015
.Sum(x=>x*x) over 1..6Seq.sumBy (fun x -> x*x)91
.GroupBy(x=>x%2) over 1..6Seq.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.

C2.07.1 · part 3

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

Related in this course