|$ curl https://forge-ai.dev/api/markdown?path=docs/js/hof
$cat docs/javascript-—-higher-order-functions.md
updated Recently·30 min read·published

JavaScript — Higher-Order Functions

JavaScriptFunctionalAdvancedAdvanced
Introduction

A higher-order function (HOF) is a function that does at least one of the following: takes one or more functions as arguments, returns a function as its result, or both. JavaScript treats functions as first-class citizens, which makes HOFs a natural and powerful part of the language. They are the cornerstone of functional programming in JavaScript.

The built-in array methods — map, filter, reduce, find, some, every, sort — are the most commonly used HOFs. Beyond these, HOFs enable advanced patterns like function composition, currying, memoization, and decorators. These patterns allow you to write more declarative, reusable, and composable code.

Higher-order functions allow you to abstract over actions, not just values. Instead of writing explicit loops and conditionals, you can express what you want to accomplish and let the HOF handle the mechanics. This leads to code that is shorter, more readable, and less prone to off-by-one errors.

hof-intro.js
JavaScript
1// HOF: takes a function as argument
2function withLogging(fn) {
3 return function(...args) {
4 console.log(`→ ${fn.name || "anonymous"}(${args})`);
5 const result = fn(...args);
6 console.log(`← ${result}`);
7 return result;
8 };
9}
10
11const add = (a, b) => a + b;
12const loggedAdd = withLogging(add);
13loggedAdd(3, 4);
14// → add(3,4)
15// ← 7
16
17// HOF: returns a function
18const multiplyBy = (factor) => (x) => x * factor;
19const double = multiplyBy(2);
20const triple = multiplyBy(3);
Functions That Take/Return Functions

The defining characteristic of a higher-order function is that it operates on functions. This can mean accepting a callback, returning a new function, or both. This ability to manipulate functions as data enables powerful abstraction patterns.

Taking Functions (Callbacks)

hof-taking-fns.js
JavaScript
1// HOF that takes a function: array methods
2const numbers = [1, 2, 3, 4, 5];
3
4// map — transform each element
5const doubled = numbers.map(n => n * 2);
6console.log(doubled); // [2, 4, 6, 8, 10]
7
8// filter — keep matching elements
9const evens = numbers.filter(n => n % 2 === 0);
10console.log(evens); // [2, 4]
11
12// Custom HOF: timing wrapper
13function timed(fn) {
14 return function(...args) {
15 const start = performance.now();
16 const result = fn(...args);
17 const elapsed = performance.now() - start;
18 console.log(`${fn.name} took ${elapsed.toFixed(2)}ms`);
19 return result;
20 };
21}
22
23const fib = (n) => n <= 1 ? n : fib(n - 1) + fib(n - 2);
24const timedFib = timed(fib);
25console.log(timedFib(40)); // logs timing info
26
27// Custom HOF: retry wrapper
28function withRetry(fn, maxRetries = 3) {
29 return async function(...args) {
30 let lastError;
31 for (let attempt = 1; attempt <= maxRetries; attempt++) {
32 try {
33 return await fn(...args);
34 } catch (error) {
35 lastError = error;
36 console.warn(`Attempt ${attempt}/${maxRetries} failed`);
37 if (attempt < maxRetries) {
38 await new Promise(r => setTimeout(r, 1000 * attempt));
39 }
40 }
41 }
42 throw lastError;
43 };
44}

Returning Functions (Factories)

hof-returning-fns.js
JavaScript
1// HOF that returns a function: factory pattern
2function createValidator(rules) {
3 return function(data) {
4 const errors = [];
5 for (const { field, validate, message } of rules) {
6 if (data[field] !== undefined && !validate(data[field])) {
7 errors.push({ field, message });
8 }
9 }
10 return { valid: errors.length === 0, errors };
11 };
12}
13
14const userValidator = createValidator([
15 { field: "name", validate: (v) => v && v.length >= 2, message: "Name too short" },
16 { field: "email", validate: (v) => /^.+@.+$/.test(v), message: "Invalid email" },
17 { field: "age", validate: (v) => v >= 18, message: "Must be 18+" },
18]);
19
20console.log(userValidator({ name: "A", email: "bad", age: 15 }));
21// { valid: false, errors: [...] }
22
23// HOF that both takes and returns: middleware pattern
24function composeMiddleware(...middlewares) {
25 return function(initialValue) {
26 return middlewares.reduce((value, middleware) => {
27 return middleware(value);
28 }, initialValue);
29 };
30}
31
32const addTimestamp = (data) => ({ ...data, timestamp: Date.now() });
33const addId = (data) => ({ ...data, id: crypto.randomUUID() });
34const sanitize = (data) => {
35 const { password, ...safe } = data;
36 return safe;
37};
38
39const processUser = composeMiddleware(addTimestamp, addId, sanitize);
40const user = processUser({ name: "Alice", password: "secret" });
41console.log(user);
42// { name: "Alice", timestamp: 1712345678, id: "uuid-..." }

info

The middleware/compose pattern (used by Express, Redux, Koa) is a pure HOF pattern. Each middleware is a function that takes data and returns (possibly modified) data. The compose HOF chains them together, creating a processing pipeline. This is function composition in action — one of the most powerful HOF patterns.
Array Higher-Order Functions

JavaScript arrays come with a rich set of built-in HOFs that cover most iteration needs. These methods replace explicit for loops with declarative operations, making code more readable and less error-prone. Understanding when to use each method is key to writing idiomatic JavaScript.

MethodReturnsUse CaseMutates Original?
mapNew array (same length)Transform every elementNo
filterNew array (subset)Select elements by conditionNo
reduceSingle accumulated valueAggregate, compute, groupNo
findFirst match (or undefined)Find single element by conditionNo
somebooleanCheck if any matchNo
everybooleanCheck if all matchNo
sortSame array (sorted)Order elementsYes (⚠)
forEachundefinedSide effects per elementNo (but can via side effects)
flatMapNew array (flattened)Map + flatten one levelNo
array-hofs.js
JavaScript
1const users = [
2 { id: 1, name: "Alice", age: 30, active: true, role: "admin" },
3 { id: 2, name: "Bob", age: 17, active: true, role: "user" },
4 { id: 3, name: "Charlie", age: 25, active: false, role: "user" },
5 { id: 4, name: "Diana", age: 35, active: true, role: "admin" },
6 { id: 5, name: "Eve", age: 22, active: true, role: "user" },
7];
8
9// map — transform
10const names = users.map(u => u.name);
11const summaries = users.map(u => `${u.name} (${u.role})`);
12
13// filter — subset
14const activeUsers = users.filter(u => u.active);
15const adults = users.filter(u => u.age >= 18);
16const admins = users.filter(u => u.role === "admin");
17
18// reduce — aggregate
19const totalAge = users.reduce((sum, u) => sum + u.age, 0);
20const averageAge = totalAge / users.length;
21
22const groupedByRole = users.reduce((groups, u) => {
23 (groups[u.role] = groups[u.role] || []).push(u);
24 return groups;
25}, {});
26
27const oldestUser = users.reduce((oldest, u) =>
28 u.age > oldest.age ? u : oldest
29);
30
31// find — first match
32const alice = users.find(u => u.name === "Alice");
33const firstAdmin = users.find(u => u.role === "admin");
34
35// some — any match
36const hasMinors = users.some(u => u.age < 18);
37const hasAnyInactive = users.some(u => !u.active);
38
39// every — all match
40const allNamed = users.every(u => u.name && u.name.length > 0);
41const allActive = users.every(u => u.active); // false
42
43// sort — with compare function (creates copy first)
44const byAge = [...users].sort((a, b) => a.age - b.age);
45const byName = [...users].sort((a, b) => a.name.localeCompare(b.name));
46
47// flatMap — map and flatten
48const tags = ["js,es6", "react,hooks"];
49const allTags = tags.flatMap(s => s.split(","));
50// ["js", "es6", "react", "hooks"]

Advanced reduce Patterns

advanced-reduce.js
JavaScript
1// reduce is the most versatile array HOF
2// Anything you can do with map/filter/find, you can do with reduce
3
4// Map with reduce
5const doubled = numbers.reduce((acc, n) => [...acc, n * 2], []);
6
7// Filter with reduce
8const evens = numbers.reduce((acc, n) => n % 2 === 0 ? [...acc, n] : acc, []);
9
10// FlatMap with reduce
11const flatMapped = arr.reduce((acc, item) => [...acc, ...fn(item)], []);
12
13// Chaining with reduce (pipeline)
14const pipeline = [n => n * 2, n => n + 1, n => `Result: ${n}`];
15const result = pipeline.reduce((value, fn) => fn(value), 5);
16console.log(result); // "Result: 11"
17
18// Run promises in sequence
19async function sequence(tasks) {
20 return tasks.reduce((promise, task) =>
21 promise.then(results => task().then(r => [...results, r])),
22 Promise.resolve([])
23 );
24}
25
26// Group by, count by, min/max
27const maxBy = (arr, fn) =>
28 arr.reduce((max, item) => fn(item) > fn(max) ? item : max);
29
30const minBy = (arr, fn) =>
31 arr.reduce((min, item) => fn(item) < fn(min) ? item : min);
32
33const countBy = (arr, fn) =>
34 arr.reduce((counts, item) => {
35 const key = fn(item);
36 counts[key] = (counts[key] || 0) + 1;
37 return counts;
38 }, {});
39
40console.log(countBy(users, u => u.role)); // { admin: 2, user: 3 }
🔥

pro tip

reduce is the Swiss Army knife of array HOFs. Every other array method (map, filter, flatMap, find) can be implemented with reduce. However, use the specific method when possible — users.filter(fn) is more readable than users.reduce(...). Use reduce when you need something the simpler methods cannot express.
Function Composition

Function composition is the process of combining two or more functions to produce a new function. The output of one function becomes the input of the next. Composition is a fundamental concept in functional programming — it allows building complex operations from simple, focused functions, creating data transformation pipelines.

function-composition.js
JavaScript
1// Compose (right-to-left)
2function compose(...fns) {
3 return (x) => fns.reduceRight((acc, fn) => fn(acc), x);
4}
5
6// Pipe (left-to-right) — more intuitive for data flow
7function pipe(...fns) {
8 return (x) => fns.reduce((acc, fn) => fn(acc), x);
9}
10
11// Example: data transformation pipeline
12const users = [
13 { name: "Alice", age: 30, active: true },
14 { name: "Bob", age: 17, active: true },
15 { name: "Charlie", age: 25, active: false },
16 { name: "Diana", age: 35, active: true }
17];
18
19// Building blocks — small, focused, reusable
20const filterActive = (arr) => arr.filter(u => u.active);
21const filterAdults = (arr) => arr.filter(u => u.age >= 18);
22const pluckName = (arr) => arr.map(u => u.name);
23const toUpper = (arr) => arr.map(n => n.toUpperCase());
24const sortAlpha = (arr) => [...arr].sort();
25
26// Compose them into a pipeline
27const getActiveAdultNames = pipe(
28 filterActive,
29 filterAdults,
30 pluckName,
31 toUpper,
32 sortAlpha
33);
34
35console.log(getActiveAdultNames(users)); // ["ALICE", "DIANA"]
36
37// Composable validation
38const required = (msg) => (v) => v ? null : msg;
39const minLength = (len) => (v) => v.length >= len ? null : `Min ${len} chars`;
40const matches = (pattern) => (v) => pattern.test(v) ? null : "Invalid format";
41const composeValidators = (...validators) => (value) =>
42 validators.reduce((errors, validate) => {
43 const error = validate(value);
44 return error ? [...errors, error] : errors;
45 }, []);
46
47const validatePassword = composeValidators(
48 required("Password is required"),
49 minLength(8),
50 matches(/[A-Z]/),
51 matches(/[0-9]/)
52);
53
54console.log(validatePassword("weak")); // ["Min 8 chars", "Must have uppercase", ...]
55console.log(validatePassword("Strong1")); // [] — valid!

info

The pipe function (left-to-right) is more intuitive for most developers because it reads like a sequence of operations: take data → filter → map → sort → output. The compose function (right-to-left) follows mathematical convention. Choose one style and be consistent. Libraries like Lodash provide _.flow (pipe) and Lodash/fp provides _.compose.
Currying & Partial Application

Currying transforms a function that takes multiple arguments into a chain of functions each taking a single argument. Partial application pre-fills some arguments of a function, returning a function that accepts the remaining arguments. Both leverage closures and are essential for building composable, reusable function pipelines.

currying-partial.js
JavaScript
1// CURRYING — each argument becomes a nested function
2const add = a => b => c => a + b + c;
3console.log(add(1)(2)(3)); // 6
4
5// Generic curry function
6function curry(fn, arity = fn.length) {
7 return function curried(...args) {
8 if (args.length >= arity) {
9 return fn(...args);
10 }
11 return (...more) => curried(...args, ...more);
12 };
13}
14
15const curriedSum = curry((a, b, c) => a + b + c);
16console.log(curriedSum(1, 2, 3)); // 6
17console.log(curriedSum(1)(2, 3)); // 6
18console.log(curriedSum(1)(2)(3)); // 6
19
20// PARTIAL APPLICATION — fix some arguments
21function partial(fn, ...presetArgs) {
22 return function(...laterArgs) {
23 return fn(...presetArgs, ...laterArgs);
24 };
25}
26
27const greet = (greeting, name) => `${greeting}, ${name}!`;
28const sayHello = partial(greet, "Hello");
29const sayGoodbye = partial(greet, "Goodbye");
30
31console.log(sayHello("Alice")); // "Hello, Alice!"
32console.log(sayGoodbye("Bob")); // "Goodbye, Bob!"
33
34// Practical: curried map/filter for composability
35const map = (fn) => (arr) => arr.map(fn);
36const filter = (pred) => (arr) => arr.filter(pred);
37const reduce = (fn, init) => (arr) => arr.reduce(fn, init);
38
39const double = map(x => x * 2);
40const isEven = filter(x => x % 2 === 0);
41const sum = reduce((a, b) => a + b, 0);
42
43const sumEvensDoubled = pipe(isEven, double, sum);
44console.log(sumEvensDoubled([1, 2, 3, 4, 5])); // (2+4)*2 = 12
45
46// Curried fetch with partial application
47const apiRequest = (method) => (url) => (data) =>
48 fetch(url, {
49 method,
50 headers: { "Content-Type": "application/json" },
51 body: data ? JSON.stringify(data) : undefined
52 }).then(r => r.json());
53
54const get = apiRequest("GET");
55const post = apiRequest("POST");
56const put = apiRequest("PUT");
57
58const getUsers = get("/api/users");
59const createUser = post("/api/users");
60
61// Using in pipeline
62const loadUserPipeline = pipe(
63 id => ({ id }),
64 ({ id }) => `/api/users/${id}`,
65 get
66);

info

Currying is most useful when combined with function composition and pipe. By currying functions like map, filter, and reduce, you create reusable building blocks that can be composed into pipelines without worrying about the data argument. This is the foundation of point-free (tacit) programming style.
Memoization

Memoization is an optimization technique that caches the results of expensive function calls and returns the cached result when the same inputs occur again. It is a classic HOF pattern — a memoize function wraps another function and manages the cache transparently. Memoization is especially valuable for pure functions with expensive computations.

memoization.js
JavaScript
1// Generic memoize HOF
2function memoize(fn) {
3 const cache = new Map();
4
5 return function(...args) {
6 const key = JSON.stringify(args);
7 if (cache.has(key)) {
8 console.log(`Cache hit: ${key}`);
9 return cache.get(key);
10 }
11
12 const result = fn(...args);
13 cache.set(key, result);
14 console.log(`Cache miss: ${key} → ${result}`);
15 return result;
16 };
17}
18
19// Expensive computation — Fibonacci with memoization
20const fib = memoize((n) => {
21 if (n <= 1) return n;
22 return fib(n - 1) + fib(n - 2);
23});
24
25// Without memoization: O(2^n)
26// With memoization: O(n)
27console.log(fib(40)); // 102334155 — fast!
28
29// Single-argument memoize with simpler cache
30function memoizeSimple(fn) {
31 const cache = {};
32 return (arg) => arg in cache ? cache[arg] : (cache[arg] = fn(arg));
33}
34
35// Memoize with TTL (time-based expiration)
36function memoizeWithTTL(fn, ttl = 60000) {
37 const cache = new Map();
38
39 return function(...args) {
40 const key = JSON.stringify(args);
41 const entry = cache.get(key);
42
43 if (entry && Date.now() - entry.timestamp < ttl) {
44 return entry.value;
45 }
46
47 const result = fn(...args);
48 cache.set(key, { value: result, timestamp: Date.now() });
49 return result;
50 };
51}
52
53// LRU cache memoize (least recently used, fixed size)
54function memoizeLRU(fn, maxSize = 100) {
55 const cache = new Map();
56
57 return function(...args) {
58 const key = JSON.stringify(args);
59
60 if (cache.has(key)) {
61 // Move to end (most recently used)
62 const value = cache.get(key);
63 cache.delete(key);
64 cache.set(key, value);
65 return value;
66 }
67
68 const result = fn(...args);
69 cache.set(key, result);
70
71 if (cache.size > maxSize) {
72 // Delete least recently used (first item)
73 const firstKey = cache.keys().next().value;
74 cache.delete(firstKey);
75 }
76
77 return result;
78 };
79}

best practice

Memoization works best with pure functions — functions that return the same output for the same input and have no side effects. The cache key is typically the stringified arguments, but for objects with circular references or non-serializable values, you may need a custom key function. Be mindful of memory usage with large caches; use TTL or LRU strategies for production applications.
Decorators (Functional Wrapping)

Decorators are higher-order functions that wrap another function to add behavior without modifying the original function's code. This is an application of the decorator pattern — a structural pattern that allows behavior to be added to individual objects or functions dynamically. JavaScript's functional nature makes this pattern especially clean.

decorators.js
JavaScript
1// Decorator: logging
2function log(fn) {
3 return function(...args) {
4 console.log(`Calling ${fn.name || "anonymous"} with:`, args);
5 const result = fn(...args);
6 console.log(`Result:`, result);
7 return result;
8 };
9}
10
11// Decorator: timing
12function time(fn) {
13 return function(...args) {
14 const start = performance.now();
15 try {
16 return fn(...args);
17 } finally {
18 console.log(`${fn.name} took ${performance.now() - start}ms`);
19 }
20 };
21}
22
23// Decorator: memoize
24function memoized(fn) {
25 const cache = new Map();
26 return function(...args) {
27 const key = JSON.stringify(args);
28 if (cache.has(key)) return cache.get(key);
29 const result = fn(...args);
30 cache.set(key, result);
31 return result;
32 };
33}
34
35// Decorator: validate
36function validate(schema) {
37 return function(fn) {
38 return function(...args) {
39 if (schema(args)) {
40 return fn(...args);
41 }
42 throw new Error("Validation failed");
43 };
44 };
45}
46
47// Decorator: retry
48function retry(maxAttempts = 3, delay = 1000) {
49 return function(fn) {
50 return async function(...args) {
51 let lastError;
52 for (let i = 0; i < maxAttempts; i++) {
53 try {
54 return await fn(...args);
55 } catch (error) {
56 lastError = error;
57 if (i < maxAttempts - 1) await new Promise(r => setTimeout(r, delay));
58 }
59 }
60 throw lastError;
61 };
62 };
63}
64
65// Combining decorators
66const getData = retry(3, 500)(time(log(async (url) => {
67 const response = await fetch(url);
68 return response.json();
69})));
70
71// Alternative: compose decorators
72function applyDecorators(fn, ...decorators) {
73 return decorators.reduce((f, decorator) => decorator(f), fn);
74}
75
76const fetchData = applyDecorators(
77 async (url) => {
78 const res = await fetch(url);
79 return res.json();
80 },
81 retry(3, 500),
82 time,
83 log
84);
🔥

pro tip

Decorators enable cross-cutting concerns — logging, timing, caching, validation, retry, access control — to be applied declaratively without polluting business logic. Each decorator is a pure HOF that can be tested independently and combined arbitrarily. This is the "composition over inheritance" principle in practice.
Pipe & Compose Patterns

pipe and compose are higher-order functions that combine multiple functions into a single function. They differ only in the order of execution: pipe runs left-to-right, compose runs right-to-left. These are the fundamental tools for building data transformation pipelines in a functional style.

pipe-compose.js
JavaScript
1// PIPE — left-to-right data flow
2function pipe(...fns) {
3 return (initial) => fns.reduce((acc, fn) => fn(acc), initial);
4}
5
6// COMPOSE — right-to-left (mathematical)
7function compose(...fns) {
8 return (initial) => fns.reduceRight((acc, fn) => fn(acc), initial);
9}
10
11// Async pipe — for Promise-returning functions
12function pipeAsync(...fns) {
13 return (initial) => fns.reduce((acc, fn) =>
14 Promise.resolve(acc).then(fn), initial
15 );
16}
17
18// Async compose
19function composeAsync(...fns) {
20 return (initial) => fns.reduceRight((acc, fn) =>
21 Promise.resolve(acc).then(fn), initial
22 );
23}
24
25// Practical: data processing pipeline
26const fetchUser = async (id) => {
27 const res = await fetch(`/api/users/${id}`);
28 return res.json();
29};
30
31const enrichWithPermissions = (user) => ({
32 ...user,
33 permissions: computePermissions(user.role)
34});
35
36const formatResponse = (user) => ({
37 data: user,
38 meta: { processedAt: Date.now() }
39});
40
41const getUserPipeline = pipeAsync(
42 fetchUser,
43 enrichWithPermissions,
44 formatResponse
45);
46
47// Conditional pipeline — add steps conditionally
48function createPipeline(...steps) {
49 return (input) => steps.reduce((acc, step) => {
50 if (typeof step === "function") return step(acc);
51 if (step.condition && step.condition(acc)) return step.transform(acc);
52 return acc;
53 }, input);
54}
55
56const processOrder = createPipeline(
57 validateOrder,
58 calculateTax,
59 { condition: (o) => o.shipping, transform: calculateShipping },
60 applyDiscount,
61 formatInvoice
62);
63
64// Tap — side effect in a pipeline without modifying data
65const tap = (fn) => (value) => {
66 fn(value);
67 return value;
68};
69
70const debugPipeline = pipe(
71 filter(isActive),
72 tap(arr => console.log("After filter:", arr.length)),
73 map(transform),
74 tap(arr => console.log("After map:", arr.length)),
75 reduce(summarize)
76);
preview

best practice

The pipe/compose pattern transforms complex logic into a series of simple, testable transformations. Each function in the pipeline has a single responsibility. Debugging is easier because you can inspect intermediate values with a tap function. For async operations, use the async variants that handle Promise chaining automatically.
$Blueprint — Engineering Documentation·Section ID: JS-HOF·Revision: 1.0