Picture a factory assembly line. A raw part rolls in one end, passes a worker who drills a hole, slides to the next who paints it, then to the next who boxes it — and a finished product rolls out the far end. Nobody has to hold the whole machine in their head; each station does one small job and hands the result along. A pipeline in code is exactly this: your data flows through a series of small steps and comes out transformed at the end.
The problem
Without pipelines, chaining a few operations means nesting function calls inside each other. Say you want to take some numbers, keep the even ones, square them, and add them up. Written as nested calls it looks like this:
List.sum (List.map (fun x -> x * x) (List.filter (fun x -> x % 2 = 0) [1..10]))
The trouble is that this reads inside-out. The first thing that happens (List.filter) is buried deep in the middle, and the last thing (List.sum) is way out on the left. Your eyes have to dig to the center, then unwind outward — the exact opposite of the order things actually happen.
The general shape h(g(f(x))) is sneaky: it grows a new layer of parentheses with every step. Add one more operation and you're hunting for which closing ) belongs to which call. The logic isn't hard — the reading is.
How it works
F# gives us the pipe operator |> to fix this. The rule is delightfully simple: |> takes the value on its left and feeds it in as the last argument to the function on its right. So x |> f is just another way of writing f x. On its own that's barely interesting — but chain a few together and the whole thing reads like a sentence, top to bottom.
[1..10]
|> List.filter (fun x -> x % 2 = 0)
|> List.map (fun x -> x * x)
|> List.sum
Read it straight down: "take 1..10, keep the evens, square them, then sum them." The order you read is the order it runs.
- StepA small function; `|>` feeds the value from one step into the next.
The animation above shows the heart of it: a value enters on the left, flows through each transform step in turn, and a final result pops out the right. Every step takes whatever the previous one produced and hands its own result to the next — no detours, no nesting, just a clean left-to-right flow you can follow with your finger.
Why it's so handy
Pipelines are exactly where transform, keep, and combine come to life — map, filter, and sum line up beautifully when you pipe between them. And the whole trick only works because F# lets you pass functions around as values: each step is a function waiting for the data to arrive.
Three nice things fall out of writing code this way:
- Readable — the steps appear in the order they happen, so the code reads like a description of what it does.
- Small and testable — each step does one little job, so you can check it on its own without untangling a giant expression.
- Easy to change — want to drop the squaring or add a new filter? Insert or delete one line; the rest of the chain doesn't care.
When a pipeline gets long, format it like the example above: one |> per line. Now each step lines up vertically, and editing a step — or commenting one out to debug — is a one-line change instead of microsurgery on a wall of parentheses.