Most people meet functions as little machines: you feed them data and they hand something back. But here's a surprise that changes everything. In a functional language, a function isn't only a machine — it's also a value, just like the number 7 or the word "hello".
That means you can keep a function in a variable, pass it to another function, and even get a function back as a result. Once you can move behaviour around as freely as data, a whole toolbox of elegant tricks opens up.
What is a higher-order function?
A higher-order function has an intimidating name and a simple meaning: it's a function that works with other functions. Either it takes a function as one of its inputs, or it hands a function back as its output — sometimes both.
Think of a kitchen robot. A normal recipe gives the robot ingredients — that's plain data. A higher-order function lets you also hand the robot a recipe — the behaviour itself, the what to do. You're no longer limited to passing flour and eggs; you can pass the very idea of "whisk this" or "bake that."
The phrase "functions are first-class" means functions get the same rights as any other value: they can be named, stored, passed, and returned. Higher-order functions are what you build with that freedom.
How it works
Let's make it real. First, we store two tiny functions in names: double and square. Notice they live in a let, exactly like a number would:
let double = fun x -> x * 2
let square = fun x -> x * x
Nothing has run yet. They're recipe cards sitting on a shelf. Now meet the kitchen robot: List.map, the tool from Transform, Keep, Combine. It takes a card and a list, and runs the card on every item.
Step through the scene below. You'll hand List.map one card, predict what comes out, then swap in a different card and predict again. Watch which parts change and which stay exactly the same.
List.map never changed. Neither did the list. The only thing you swapped was the function you handed in, and that alone decided what came out. That's the whole point: List.map handles the looping, the card handles the what to do.
And List.map isn't special. You can write your own function that takes a function. Here's applyTwice, whose first input f is a function that it runs two times:
// A higher-order function: its first input, f, IS a function
let applyTwice f x = f (f x)
// Pass a function in as if it were data
applyTwice square 3 // square (square 3) = 81
applyTwice double 3 // double (double 3) = 12
Try reading applyTwice square 3 out loud: "apply square twice to 3." The behaviour (square) and the data (3) sit side by side as ordinary arguments — that's the whole trick.
You have let shout = fun (s: string) -> s.ToUpper() and a list called names. Which line gives you every name in capitals?
A quick word on partial application
Because functions are values, you can also pre-fill some of their inputs to make a smaller, specialized function. That's called partial application, and it sounds fancier than it is.
Imagine an add function that wants two numbers. If you give it just one, say 5, you don't get an error. You get back a new function that's still waiting for the second number:
let add a b = a + b
let add5 = add 5 // a new function, waiting for b
add5 10 // 15
You've built a custom add5 tool out of a more general one, without writing it from scratch.
Here's a more useful one. addPercent adds a percentage to a price. Give it just the percent and you get a ready-made withTax function:
let addPercent pct price =
price + price * pct / 100
let withTax = addPercent 20
Step through it below and make two predictions: what withTax is, and what withTax 100 returns. Then flip the input order switch to see what happens when the same function is written with price first.
Put the input you want to pre-fill first. F# always fills a function's inputs from the left, so addPercent 20 fills whichever input comes first. If that's price, you get a function for "a $20 item at some percent", and because both inputs are whole numbers, F# can't spot the mix-up: withTax 100 quietly returns 40 instead of 120. Settings like a rate or a separator go first; the data that changes every call goes last.
You have let discount amount price = price - amount, and you want a 'take $5 off' function to hand to List.map. What do you write?
Why it matters
Passing behaviour around is the quiet engine behind some of the most loved tools in programming. The transform/keep/combine trio of map-filter-reduce works precisely because you hand each of them a small function describing what to do to every item.
It's also how you snap small steps together into pipelines: each stage is just a function, so the whole flow becomes a sequence of behaviours you can rearrange, reuse, and reason about. Once functions are values, your code stops being a pile of fixed instructions and becomes a set of building blocks you can mix freely.