all_lessons/SICP JavaScript/03 · Higher-order functionslesson 3 / 22

Higher-order functions

Lesson 02 left us re-typing the same control skeletons — accumulate-while-counting, shrink-and-recombine — with only the combining step changing, and named that repetition as the pressure that demands a new abstraction. This lesson grants it. We make functions first-class values: things you can name, pass as arguments, and return as results, exactly like numbers. A function that takes or returns a function is a higher-order function. This is not syntactic convenience — it is a genuine means of abstraction that lets you name a pattern of computation, the thing lesson 02 could not name. We build it up to its sharpest payoff: a single fixedPoint machine that, fed the right function, computes square roots.

Book source
Maps to SICP §1.3 (Formulating Abstractions with Higher-Order Procedures) — functions as arguments, lambda and local names, functions as returned values, and fixed points / average damping with Newton's method. We follow that arc; the JavaScript, the sum/accumulate generalization, and the fixed-point trace are original.
Linear position
Prerequisite: Lesson 02 (definition vs. process; the repeated control patterns across factorial, fib, expt, gcd) and Lesson 01 (the substitution model and procedural abstraction).
New capability: treat functions as first-class values — pass them in and return them out — to capture a general method as a named, reusable abstraction; express summation, products, and accumulation as one function; and build the fixed-point search that yields sqrt as a special case.
The plan
Five moves. (1) Functions as arguments: factor the repeated sum pattern into one higher-order function. (2) Generalize again to accumulate, and meet lambda and let-as-lambda. (3) Functions as returned values: build new functions at runtime (average damping). (4) Fixed points: a general search method, and Newton's method generalized. (5) Express sqrt as a fixed point — the payoff. Then the pressure that forces lesson 04.

1 · Functions as arguments — naming a pattern

Three functions: sum of integers a..b, sum of their cubes, and a series for π/8. Written separately they share a skeleton and differ in exactly two spots — the term applied to each value and how to get the next value:

function sumInts(a, b)  { return a > b ? 0 : a       + sumInts(a + 1, b); }
function sumCubes(a, b) { return a > b ? 0 : a*a*a   + sumCubes(a + 1, b); }
function piSum(a, b)    { return a > b ? 0 : 1/(a*(a+2)) + piSum(a + 4, b); }

The repetition lesson 02 warned about, in the flesh. Because functions are first-class, we can pass the two varying pieces as functions and write the skeleton once. This is the higher-order function sum — itself the mathematical idea of summation, captured as code:

function sum(term, a, next, b) {        // term and next are FUNCTIONS
  return a > b ? 0 : term(a) + sum(term, next(a), next, b);
}
const id    = x => x;
const inc   = x => x + 1;
const cube  = x => x * x * x;

const sumInts2  = (a, b) => sum(id,   a, inc, b);
const sumCubes2 = (a, b) => sum(cube, a, inc, b);

We have named the pattern itself. sum is an abstraction over a whole family of functions, parameterized by behavior, not just by data. That is the leap lesson 02's means of abstraction could not make — it could name a process, but not a pattern shared across processes — and it is why first-classness is an abstraction mechanism, not syntax sugar.

2 · Generalize again — accumulate, lambda, and let-as-lambda

sum hard-wires + and a base of 0. Abstract those too and you get accumulate, of which sum and product are both special cases:

function accumulate(combiner, nullValue, term, a, next, b) {
  return a > b ? nullValue
              : combiner(term(a), accumulate(combiner, nullValue, term, next(a), next, b));
}
const sum2     = (term, a, next, b) => accumulate((x, y) => x + y, 0, term, a, next, b);
const product2 = (term, a, next, b) => accumulate((x, y) => x * y, 1, term, a, next, b);
const factA    = n => product2(id, 1, inc, n);   // factorial, for free

The anonymous functions (x, y) => x + y are lambdas — function values written inline without a declared name, used exactly where a one-off function is needed. A lambda is the thing a function declaration names; the declaration is just a lambda plus a name. The same identity explains local names: a local binding (let r = ... inside a body) is sugar for immediately applying a lambda whose parameter is that name. So "introduce a local name" and "apply an anonymous function" are the same operation — a fact lesson 09 and 10 will exploit when we model scope precisely.

let x = e1 in body is exactly ((x) => body)(e1) — a local name is an applied lambda

3 · Functions as returned values — building functions at runtime

First-classness cuts both ways: a function can return a function. This lets us build new functions programmatically. Average damping takes any function f and returns a new function that averages f(x) with x — a transformation on functions:

const averageDamp = f => (x => (x + f(x)) / 2);   // takes a fn, RETURNS a fn

averageDamp(x => x * x)(10);   // = (10 + 100)/2 = 55

The returned arrow closes over f — it remembers the f it was built with. (Exactly how a returned function remembers its birth environment is the question the substitution model handles only by hand-waving; lesson 10's environment model answers it precisely. Note the IOU.) Damping matters because it stabilizes iterative searches that would otherwise oscillate, as we are about to see.

4 · Fixed points — a general method, and Newton generalized

A fixed point of a function f is a value x with f(x) = x. For many functions you can find one by iterating: apply f repeatedly until the value stops changing. That search is itself a higher-order function — it takes the function whose fixed point we want:

function fixedPoint(f, start) {
  const tol = 1e-7;
  function iter(guess) {
    const next = f(guess);
    return Math.abs(next - guess) < tol ? next : iter(next);
  }
  return iter(start);
}
Worked example — fixed point of cosine
fixedPoint(Math.cos, 1.0):
 guess     cos(guess)
 1.00000   0.54030
 0.54030   0.85755
 0.85755   0.65429
 0.65429   0.79348
   ...        ...
 0.73909   0.73909   <- converged: cos(x) = x at x = 0.73909
Repeatedly pressing the cos button on a calculator does exactly this. The search machine is general; cosine is just one function fed to it.

Newton's method (lesson 01's sqrt engine) is itself just fixed-point search of a derived function. The root of g is a fixed point of x − g(x)/g′(x). So newtonsMethod is a higher-order function that takes g, builds that derived function, and hands it to fixedPoint. We have abstracted the abstraction.

5 · sqrt as a fixed point — the payoff

√x is the value y with y = x/y — i.e. a fixed point of y ↦ x/y. Naively iterating that oscillates (it bounces between guesses and never settles), which is precisely what average damping fixes. So:

const sqrt = x => fixedPoint(averageDamp(y => x / y), 1.0);

sqrt(2);   // => 1.41421356...

Stare at that one line. Lesson 01's sqrt was a custom block with hand-written improve, isGoodEnough, and iter helpers. Here sqrt is assembled from independently reusable higher-order parts — fixedPoint (a general search), averageDamp (a general stabilizer), and a one-line lambda for the specific equation. Each part is a named pattern, testable and reusable on its own; sqrt is their composition. That is the entire promise of higher-order functions cashed out: programs built by snapping together named methods, not by re-coding skeletons.

Common mistakes / failure modes

Calling vs. passing a function
sum(cube, ...) passes the function value cube; sum(cube(2), ...) passes the number 8. A higher-order function wants the function itself — the name without the parentheses.
"It's just shorthand"
Treating lambdas/HOFs as cosmetic misses the point: they let you name a pattern across functions, an abstraction lesson 02's tools literally could not express. The reusability of sum/fixedPoint is the substance.
Forgetting damping in oscillating searches
The fixed point of y ↦ x/y oscillates forever without average damping. The general search is correct; some functions need stabilizing before iteration converges.
Hand-waving how a returned function remembers
averageDamp(f) returns a function that recalls f. The substitution model can only mumble about this; do not pretend it is fully explained until the environment model (lesson 10) defines what "remember" means.

Checkpoint exercise

Try it
(1) Using product (the accumulate special case above), write factorial(n) and a function that approximates π via the Wallis product (π/4 ≈ (2·4·4·6·6·8…)/(3·3·5·5·7·7…)). (2) Define compose(f, g) that returns the function x ↦ f(g(x)), and use it to build inc4 = compose(inc, compose(inc, compose(inc, inc))); what is inc4(0)? (3) Explain in one sentence why sqrt = fixedPoint(averageDamp(y => x / y), 1.0) needs the averageDamp.
const compose = (f, g) => (x => f(g(x)));
Answers: (1) factorial = n => product2(id, 1, inc, n); the Wallis approximation is a product over terms 2k/(2k−1) · 2k/(2k+1) — the skeleton is reused, only the term/next change. (2) compose returns a function, the canonical "function-returning function"; inc4(0) = 4. (3) Iterating y ↦ x/y alone oscillates between two values and never converges; averaging each step with the previous guess damps the oscillation so the iteration settles on √x.

Where this points next

We can now abstract over behavior: a control pattern is a value with a name, and programs are composed from reusable methods rather than hand-coded skeletons. But notice what every function in this entire part has operated on — numbers. sum, fixedPoint, sqrt: all push single numeric values through. Real programs manipulate compound things — a rational number is a numerator and a denominator, a point is an x and a y, a record is many fields glued together — and they need to glue those parts into a unit, pass it around as one object, and pull the parts back out, all without callers knowing how the gluing is done. Higher-order functions gave us abstraction over procedures; they give us nothing for building and hiding compound data. That gap — programs need to manipulate compound data behind a boundary, just as sqrt hid its process behind a contract — is the pressure that forces lesson 04: data abstraction, where a data type becomes a contract between constructors and selectors separated by an abstraction barrier.

Takeaway
Making functions first-class values — nameable, passable as arguments, returnable as results — turns a control pattern into a named, reusable abstraction, which is exactly what lesson 02's repeated skeletons demanded and its means of abstraction could not provide. Passing the varying pieces as functions collapses sumInts/sumCubes/piSum into one sum; abstracting the combiner and base yields accumulate, with sum, product, and even factorial as special cases. Lambdas are unnamed function values (a declaration is just a lambda plus a name), and a local binding is an applied lambda — the same operation. Functions can also be returned (averageDamp builds a new function that closes over its input), enabling general machines like fixedPoint and a generalized Newton's method. The payoff: sqrt = fixedPoint(averageDamp(y => x/y), 1.0) — square root assembled from independently reusable higher-order parts. First-classness is an abstraction mechanism, not syntax. But everything here pushes numbers; real programs need to glue and hide compound data, which forces data abstraction next.

Interview prompts