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 a tiny function in a variable called square — notice it lives in a let just like a number would.
Then we write applyTwice, a function whose first argument is another function. It takes some behaviour f and a value x, and runs f on x twice. When we pass square into it, the function travels in as if it were a piece of data — and that's exactly what the animation below shows.
- Higher-order functionTakes (or returns) another function — you pass behaviour around like data.
Here it is in F#. The first line ties the name square to a function value. The second line defines a higher-order function. The last line drops square into it like any other argument.
// A function stored in a variable, just like a value
let square = fun x -> x * x
// A higher-order function: its first argument, f, IS a function
let applyTwice f x = f (f x)
// Pass the square function in as if it were data
applyTwice square 3 // square (square 3) = 81
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.
A quick word on partial application
- addA general function wanting two numbers.
- add5Made by pre-filling the first argument with 5 — a smaller, reusable tool still waiting for the second number.
Because functions are values, you can also pre-fill some of their arguments 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. You've built a custom add5 tool out of a more general one, without writing it from scratch.
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.