Almost every program ends up doing the same three things to lists, over and over: changing every item into something else, throwing away the items you don't want, and squeezing a pile of items down to one answer. You could hand-write a loop for each of these every single time. But there's a friendlier way — three little tools with friendly names: Transform, Keep, and Combine.
Here's a nice way to picture it. Think of your list like a tiny database table, and these three tools as the queries you run over it — you select new columns, you keep the rows that match a where clause, and you aggregate everything into a grand total. Same idea, but on any list you've got, right there in your code.
Each of these tools takes a function as a value — you hand it a little rule, and it does the looping for you. That trick has its own name: a higher-order function. You describe what you want done; the tool figures out how to walk the list.
Transform
Transform (you'll also hear it called map or select) takes a function and applies it to every single item, handing you back a brand-new list of the same length. Nothing is added, nothing is dropped — each item just gets made over. Want to double every number? Give Transform the rule "multiply by two" and it does the rest.
In F# the tool is called List.map. You pass it the rule and the list, and out comes the made-over list:
// Double every number
List.map (fun x -> x * 2) [1; 2; 3]
// => [2; 4; 6]
Three numbers go in, three numbers come out — each one transformed by the rule you supplied.
Let's give it something real to chew on. A little shop has five order totals: [ 20; 45; 8; 30; 12 ] dollars, and every dollar earns 2 loyalty points. Step through the scene below: after you see the first order go through, predict the whole list that List.map hands back. Keep an eye on the count, and on the original list.
Keep
Keep (the classic names are filter or where) is the bouncer at the door. You give it a yes-or-no test, and it walks the list letting only the items that pass through. The ones that fail are simply left behind, so unlike Transform, the list can come out smaller than it went in.
In F# it's List.filter. Hand it a test that returns true or false, and it keeps the trues:
// Keep only the even numbers
List.filter (fun x -> x % 2 = 0) [1..6]
// => [2; 4; 6]
Six numbers go in, but only the three that pass the "is it even?" test make it out. This is your where clause: keep the rows that match.
Back to the shop. Only orders of $20 or more earn points, so the test is fun o -> o >= 20. Before you step through, predict how many of the five orders make it. Careful with the one that's exactly $20: >= means "20 or more", so it counts.
Combine
Combine (known as fold, reduce, or aggregate) is the one that boils a whole list down into a single value. Think of a running total: you start with a number, then walk the list adding each item to what you've got so far, until one final answer is all that's left.
In F# the tool is List.fold. You give it a combining rule, a starting value, and the list:
// Add everything up, starting from 0
List.fold (+) 0 [1; 2; 3; 4]
// => 10
It grabs the start (0), folds in 1, then 2, then 3, then 4, and the four numbers collapse into the grand total 10. That's your aggregate: one number that summarizes the whole list.
The rule you give List.fold takes two things: the answer so far (here we call it sum) and the next item. Whatever the rule returns becomes the new answer so far for the next item. Step through the fold below and watch sum slide along the orders, changing at every step. Partway through, you'll be asked what it will be a couple of orders later.
You want to know how many orders were over $100. Which of these gives you that number?
Better than a loop
Why bother, when a plain loop can do all of this? Because these three tools say what you mean instead of how to do it. "Keep the even ones, then double them, then add them up" reads almost like plain English — no counters to manage, no off-by-one mistakes, no temporary list to remember to clear out. The intent is right there on the surface.
And the real magic is that they snap together. Because each one takes a list and (mostly) gives back a list, you can line them up so the output of one flows straight into the next. Stringing them together this way is called a pipeline, and the |> symbol means "take this and hand it to the next step". Here's the shop's whole question, "how many points did the big orders earn?", as one pipeline:
orders
|> List.filter (fun o -> o >= 20) // Keep the big orders
|> List.map (fun o -> o * 2) // Transform: 2 points per dollar
|> List.sum // Combine: add them up
List.sum is just a ready-made Combine: it does the same job as List.fold (+) 0.
Predict the total before you step through. Then flip the switch to see the very same job written as a hand-made loop, and count how many things you have to keep in your head.
The order of the steps matters. Swap the first two lines, so you double every order before testing it, and the total jumps from 190 to 214. Why? The $12 order becomes 24 points, which now passes the >= 20 test even though the order itself was small. Each step only sees what the step before it handed over, so ask: am I testing the original items, or the transformed ones?
When you catch yourself writing a loop over a list, pause and ask which of the three you're really doing. Changing every item? That's Transform. Picking some out? That's Keep. Reducing to one answer? That's Combine. Most loops are just one of these three wearing a disguise.
An old loop walks a list of words and glues each one onto a growing sentence, ending with one long string. Which of the three is it really?