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.
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 |> fisf x; it places the list as the last argument, so stages read top to bottom.
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.
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
| pipeline | list 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.
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
- Microsoft — F# lists —
map,filter,choose, and the pipe operator. - FSharp.Core API reference —
List.map,List.filter,List.choose, and theirArray/Seqsiblings. - Microsoft — LINQ in C# —
SelectandWhere, the C# transform-and-keep operators.
Related in this course
- C2.05.2 — From the accumulator loop to fold — collapse a list, where these build one.
- C1.07 — Collections: list, array, seq, Map & Set — the structures and the lazy
seqversions. - C1.03 — Functions, currying & partial application — the functions you pipe into
mapandfilter.