jonnifyfsharp

Dive C2.05.3

From the transform loop to map & filter

Build the new list by naming the transform, not by indexing into a target.

When a C# loop builds a new collection — square every number, keep the even ones — it declares a target list and appends to it: var result = new List<int>(); foreach (var x in xs) if (x % 2 == 0) result.Add(x * x);. The index, the target, and the Add are all mechanics. F# names the transform itself. List.map applies a function to every element; List.filter keeps the elements a predicate accepts; List.choose does both at once. The pipe |> chains them so the data flows top to bottom, and there is no index to get wrong and no target to mutate. This dive is the third day-one habit, and it sets up LINQ-on-seq in C2.07.

C2.05.3 · part 1

map transforms, filter keeps

The two basic list-builders split the loop's job in two. List.map takes a function and returns a list of the same length with that function applied to each element — a transform. List.filter takes a predicate and returns the sublist of elements for which it holds — a keep. Neither needs an index or a mutable target; the pipe |> sends the list in from the left. Each echo below was produced by dotnet fsi.

// map: apply a function to every element (same length)
[1..5]  |> List.map (fun x -> x * x)        // val it : int list = [1; 4; 9; 16; 25]

// filter: keep the elements a predicate accepts
[1..10] |> List.filter (fun x -> x % 2 = 0)  // val it : int list = [2; 4; 6; 8; 10]
map
applies a function to each element; the result is the same length, every element transformed.
filter
keeps the elements for which the predicate is true; the result is a sublist, order preserved.
|>
the pipe — x |> f is f x; it places the list as the last argument, so stages read top to bottom.
C2.05.3 · part 2

choose: transform and keep in one pass

The loop that filters and transforms is common enough to have its own function. List.choose takes a function returning an Option: it keeps the Some values, unwrapped, and drops the None values. So the decision "is this element wanted?" and the transform "what does it become?" are made together. The example keeps the even numbers and squares them in a single pass.

// choose: keep-and-transform — Some keeps (unwrapped), None drops
[1..6] |> List.choose (fun x -> if x % 2 = 0 then Some (x * x) else None)
// val it : int list = [4; 16; 36]

// the same result as filter-then-map, but one walk:
[1..6] |> List.filter (fun x -> x % 2 = 0) |> List.map (fun x -> x * x)
// val it : int list = [4; 16; 36]

The two pipelines land on the same list. filter-then-map walks the data twice and reads clearly as two steps; choose walks once and folds the decision into the transform. Both are pipelines of named stages — what differs from the C# loop is that neither carries an index or a target list, so the off-by-one and the wrong-target bugs have nowhere to live.

These functions build a brand-new list and leave the input untouched — F# lists are immutable, so map cannot change the source the way a loop mutating a target would. The sibling module Array has the same functions for arrays, and Seq has the lazy versions covered in C2.07 (LINQ → seq).

Build a pipeline over [1..6]

Toggle the stages and watch the list flow through. Start from [1; 2; 3; 4; 5; 6]; turn on filter even to keep [2; 4; 6], then map square to reach [4; 16; 36]. The choose stage does both in one pass and lands on the same list. The readout shows the list after each enabled stage — the exact values dotnet fsi returns.

[1..6] |> stage |> stage

source [1..6] List.filter x % 2 = 0 List.map x * x List.choose even → x*x result [4; 16; 36]
[1; 2; 3; 4; 5; 6] → filter even [2; 4; 6] → map square → [4; 16; 36]
Without JavaScript: the pipeline stages over [1..6]
pipelinelist after the stage
[1..6] (source)[1; 2; 3; 4; 5; 6]
|> List.filter (x % 2 = 0)[2; 4; 6]
|> List.map (x * x)[4; 16; 36]
|> List.choose (even → x*x) (one pass)[4; 16; 36]

A pipeline is named stages, read top to bottom. The loop's index, target, and append are gone, and with them the bugs that lived in the mechanics.

C2.05.3 · part 3

The C# build-a-list loop, beside the pipeline

The C# loop that builds a list declares a target, walks the source with a guard, and appends the transformed element. Every piece of that — the new List, the if, the Add — is the mechanics F# moves into filter, map, or choose. A C# developer who reaches for LINQ already writes the pipeline; in F# the pipeline is the ordinary way to build a list.

C# — declare a target, guard, append

var result = new List<int>();
foreach (var x in xs)
    if (x % 2 == 0)
        result.Add(x * x);        // guard, then transform, then append
// xs = 1..6  ->  result = [4, 16, 36]

F# — a pipeline of named stages

// filter then map — reads as two steps
[1..6] |> List.filter (fun x -> x % 2 = 0) |> List.map (fun x -> x * x)
// val it : int list = [4; 16; 36]

// or choose — guard and transform in one
[1..6] |> List.choose (fun x -> if x % 2 = 0 then Some (x * x) else None)
// val it : int list = [4; 16; 36]

C# habit

A loop with a target list — a guard, a transform, an Add — or LINQ's .Where(…).Select(…) when reaching for it.

F# form

List.map / List.filter / List.choose, chained with |> — no index, no target, no mutation.

Where this leads

The three workhorses are now in hand: map and filter build lists, fold collapses one, and the pipe chains them. The same operators live on seq, where they are lazy — computed only as far as the result is read — which is exactly how LINQ's IEnumerable<T> behaves. That equivalence, and the LINQ-to-seq operator map, is the next chapter step (C2.07).

References

Sources

Related in this course