<arch.design/>
Principles/Higher-Order Functions
{ }CodeArchitecturebeginner1958functionalmapfilterreduce

Higher-Order Functions

Functions that accept other functions as arguments or return them — enabling composable, reusable abstractions without classes or inheritance.

4/5
{ }
Operates at: Code level

Inside a codebase — classes, modules, files

How it works

A higher-order function (HOF) is a function that does at least one of: takes one or more functions as arguments; returns a function as its result. The concept comes from mathematics (functors, combinators) and entered programming with Lisp (1958).

The canonical HOFs are map, filter, and reduce — they abstract over looping and let you express transformations declaratively. Instead of a for loop that iterates, checks a condition, and accumulates a result, you compose map(transform).filter(predicate).reduce(combine).

HOFs also enable partial application and currying: a function that takes some arguments and returns a new function waiting for the rest. This produces reusable, composable building blocks — a logger that pre-captures the context, a validator that pre-captures the rules, a fetch that pre-captures the base URL.

In TypeScript, Go, and Rust, functions are first-class values — they can be stored in variables, passed as arguments, and returned. This is all HOFs require. No classes needed.

Implementation

TypeScript · Go · Rust
const double   = (n: number) => n * 2;
const isEven   = (n: number) => n % 2 === 0;
const toString = (n: number) => `${n}`;

// map / filter / reduce — canonical higher-order functions
const result = [1, 2, 3, 4, 5]
  .filter(isEven)   // [2, 4]
  .map(double)      // [4, 8]
  .map(toString);   // ["4", "8"]

// HOF that returns a function — adds behaviour without changing the original
function withLogging<T extends unknown[], R>(
  name: string,
  fn: (...args: T) => R
): (...args: T) => R {
  return (...args) => {
    console.log(`[${name}] args:`, args);
    const result = fn(...args);
    console.log(`[${name}] result:`, result);
    return result;
  };
}

const loggedDouble = withLogging("double", double);
loggedDouble(5); // logs input and output, returns 10

// Partial application — pre-fill some arguments
const multiply = (a: number) => (b: number) => a * b;
const triple   = multiply(3);
[1, 2, 3].map(triple); // [3, 6, 9]

Why it matters

HOFs eliminate repetition at the logic level, not just the data level. Instead of copying a loop pattern three times with different bodies, you write one function and pass the varying behaviour as a parameter. The result is more expressive, shorter, and easier to test.

When to use

  • Data transformations (map, filter, reduce) over collections
  • Middleware pipelines (HTTP, Redux, Express) — each step is a function
  • Decorator / wrapping pattern — add logging, caching, retry by wrapping a function
  • Partial application to create specialised versions of a general function

When NOT to use

  • When the function being passed is very complex — a named class method may be more readable
  • Deep nesting of HOFs becomes harder to debug than an equivalent imperative loop

Trade-offs

+

Eliminate boilerplate loops — express intent, not mechanics

Deeply chained HOFs can be hard to debug (no named variables at each step)

+

Composable building blocks — combine small functions into complex pipelines

Performance cost of creating many closure objects in tight loops

+

Functions become data — storable, passable, configurable

Stack traces through HOFs and closures are harder to read

In production

React

Higher-order components (HOC) — wrap a component and return an enhanced component. Custom hooks are HOFs that return reactive values

Express / Koa

Middleware is a HOF — each layer takes (req, res, next) and returns nothing, but wraps the next layer

RxJS

Operators (map, filter, mergeMap, debounceTime) are HOFs over Observables — compose entire async pipelines from small functions

Industry adoption

4/5Widely adopted — mainstream at medium-to-large engineering orgs.

Related principles