4.4 Higher-Order Functions and Functional Programming
Functional programming (FP) is a paradigm that structures programs around pure functions and immutable data. Because functions are first-class citizens in JavaScript, functional style comes naturally.
Pure Functions
A pure function satisfies two conditions:
- Same input → always same output
- No side effects — does not modify external state
// ✅ Pure functions — predictable, easy to test
function add(a, b) { return a + b; }
function double(arr) { return arr.map(x => x * 2); }
const uppercase = (str) => str.toUpperCase();
// ❌ Impure functions — depend on / modify external state
let tax = 0.1;
function calcPrice(price) {
return price * (1 + tax); // depends on external variable — impure
}
function pushItem(arr, item) {
arr.push(item); // mutates argument — side effect
return arr;
}
// ✅ Refactored as pure
function calcPricePure(price, taxRate) {
return price * (1 + taxRate); // all dependencies passed as arguments
}
function pushItemPure(arr, item) {
return [...arr, item]; // returns a new array
}
const original = [1, 2, 3];
const withFour = pushItemPure(original, 4);
console.log(original); // [1, 2, 3] — unchanged
console.log(withFour); // [1, 2, 3, 4]
Immutability
Create new data instead of mutating existing data. This makes bugs easier to track and improves predictability.
// Object.freeze — shallow freeze
const config = Object.freeze({
apiURL: "https://api.example.com",
timeout: 5000,
retries: 3,
});
// config.apiURL = "http://hacker.com"; // TypeError (strict mode)
// config.retries++; // TypeError
// Nested objects are NOT frozen (shallow freeze)
const shallow = Object.freeze({ nested: { value: 1 } });
shallow.nested.value = 99; // Allowed! (nested itself is not frozen)
// Deep freeze
function deepFreeze(obj) {
Object.getOwnPropertyNames(obj).forEach(name => {
const value = obj[name];
if (typeof value === "object" && value !== null) {
deepFreeze(value);
}
});
return Object.freeze(obj);
}
const immutableConfig = deepFreeze({
server: { host: "localhost", port: 3000 },
db: { host: "localhost", port: 5432 },
});
// immutableConfig.server.port = 8080; // TypeError!
Immutable updates with spread pattern
const user = { name: "Alice", age: 30, role: "user" };
// Update one field
const updatedUser = { ...user, age: 31 };
console.log(user); // { name: "Alice", age: 30, role: "user" }
console.log(updatedUser); // { name: "Alice", age: 31, role: "user" }
// Immutable update for nested objects
const state = {
users: [
{ id: 1, name: "Alice", settings: { theme: "dark" } },
{ id: 2, name: "Bob", settings: { theme: "light" } },
],
};
// Change Alice's theme
const newState = {
...state,
users: state.users.map(u =>
u.id === 1
? { ...u, settings: { ...u.settings, theme: "light" } }
: u
),
};
console.log(state.users[0].settings.theme); // "dark" — original preserved
console.log(newState.users[0].settings.theme); // "light"
Higher-Order Functions
Functions that take a function as an argument or return a function.
// Higher-order function that takes a function as argument
function applyTwice(fn, value) {
return fn(fn(value));
}
const double = x => x * 2;
const addTen = x => x + 10;
console.log(applyTwice(double, 3)); // 12 (3 → 6 → 12)
console.log(applyTwice(addTen, 5)); // 25 (5 → 15 → 25)
// Higher-order function that returns a function
function multiplier(factor) {
return x => x * factor;
}
const triple = multiplier(3);
const fiveTimes = multiplier(5);
console.log([1, 2, 3, 4, 5].map(triple)); // [3, 6, 9, 12, 15]
console.log([1, 2, 3, 4, 5].map(fiveTimes)); // [5, 10, 15, 20, 25]
Currying
Transforms a function that takes multiple arguments into a chain of functions each taking one argument.
// Curried function — takes one argument at a time
const curriedAdd = a => b => c => a + b + c;
console.log(curriedAdd(1)(2)(3)); // 6
// Partial application creates reusable functions
const add10 = curriedAdd(10);
const add10and20 = add10(20);
console.log(add10(5)(3)); // 18
console.log(add10and20(7)); // 37
// Auto-curry utility
function curry(fn) {
const arity = fn.length; // number of function parameters
return function curried(...args) {
if (args.length >= arity) {
return fn(...args);
}
return function(...moreArgs) {
return curried(...args, ...moreArgs);
};
};
}
// Create curried versions
const add = curry((a, b, c) => a + b + c);
const multiply = curry((a, b) => a * b);
const includes = curry((target, arr) => arr.includes(target));
console.log(add(1)(2)(3)); // 6
console.log(add(1, 2)(3)); // 6
console.log(add(1)(2, 3)); // 6
console.log(add(1, 2, 3)); // 6
const double2 = multiply(2);
console.log([1, 2, 3, 4, 5].map(double2)); // [2, 4, 6, 8, 10]
const hasAdmin = includes("admin");
console.log(hasAdmin(["user", "admin", "guest"])); // true
Function Composition
Combining multiple functions into one by chaining them.
// compose: applies right to left (mathematical composition)
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
// pipe: applies left to right (more readable)
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
// Example functions
const trim = str => str.trim();
const lowercase = str => str.toLowerCase();
const replaceSpaces = str => str.replace(/\s+/g, "-");
const addPrefix = str => `post-${str}`;
// pipe: data transformation pipeline
const createSlug = pipe(
trim,
lowercase,
replaceSpaces,
addPrefix,
);
console.log(createSlug(" Hello World ")); // "post-hello-world"
console.log(createSlug(" My Blog Post ")); // "post-my-blog-post"
// compose (reverse direction)
const createSlugCompose = compose(
addPrefix,
replaceSpaces,
lowercase,
trim,
);
console.log(createSlugCompose(" Hello World ")); // "post-hello-world"
Async pipeline
// pipeAsync for async functions
const pipeAsync = (...fns) => x =>
fns.reduce((p, f) => p.then(f), Promise.resolve(x));
// Example: data processing pipeline
const fetchUser = async (id) => ({ id, name: "Alice", role: "admin" });
const enrichUser = async (user) => ({ ...user, permissions: ["read", "write"] });
const formatUser = async (user) => ({
...user,
displayName: user.name.toUpperCase(),
canWrite: user.permissions.includes("write"),
});
const processUser = pipeAsync(fetchUser, enrichUser, formatUser);
processUser(1).then(user => {
console.log(user.displayName); // "ALICE"
console.log(user.canWrite); // true
});
Partial Application
Pre-filling some arguments of a function to create a new function with fewer parameters.
// Partial application utility
function partial(fn, ...presetArgs) {
return function(...laterArgs) {
return fn(...presetArgs, ...laterArgs);
};
}
// HTTP request function (use fetch in real apps)
async function request(baseURL, method, endpoint, data) {
const url = `${baseURL}${endpoint}`;
console.log(`${method} ${url}`, data ?? "");
return { method, url, data };
}
// Lock in base URL
const apiRequest = partial(request, "https://api.example.com");
// Lock in method
const get = partial(apiRequest, "GET");
const post = partial(apiRequest, "POST");
const del = partial(apiRequest, "DELETE");
// Clean usage
get("/users");
post("/users", { name: "Alice" });
del("/users/1");
Practical Example: Functional Data Transformation Pipeline
// E-commerce order processing pipeline
const orders = [
{ id: 1, customer: "Alice", items: ["book", "pen"], total: 15.99, status: "pending" },
{ id: 2, customer: "Bob", items: ["laptop"], total: 999.99, status: "paid" },
{ id: 3, customer: "Carol", items: ["notebook", "pencil", "ruler"], total: 8.50, status: "pending" },
{ id: 4, customer: "Dave", items: ["phone"], total: 599.99, status: "cancelled" },
{ id: 5, customer: "Eve", items: ["tablet", "case"], total: 449.99, status: "paid" },
];
// Pure transformation functions
const filterByStatus = curry((status, orders) =>
orders.filter(o => o.status === status)
);
const sortByTotal = (orders) =>
[...orders].sort((a, b) => b.total - a.total);
const addTax = curry((taxRate, orders) =>
orders.map(o => ({ ...o, taxAmount: +(o.total * taxRate).toFixed(2) }))
);
const formatOrder = (order) => ({
orderId: `#${String(order.id).padStart(4, "0")}`,
customer: order.customer,
itemCount: order.items.length,
subtotal: `$${order.total.toFixed(2)}`,
tax: `$${(order.taxAmount ?? 0).toFixed(2)}`,
total: `$${(order.total + (order.taxAmount ?? 0)).toFixed(2)}`,
});
// Compose the pipeline
const processPaidOrders = pipe(
filterByStatus("paid"),
sortByTotal,
addTax(0.1),
(orders) => orders.map(formatOrder),
);
const result = processPaidOrders(orders);
result.forEach(o => {
console.log(`${o.orderId} | ${o.customer} | ${o.itemCount} items | ${o.total}`);
});
// #0002 | Bob | 1 items | $1099.99
// #0005 | Eve | 2 items | $494.99
Transducer Pattern
An advanced technique to avoid intermediate array creation in map + filter chains for large datasets.
// Regular approach: creates 2 intermediate arrays
const result1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
.filter(x => x % 2 === 0) // [2, 4, 6, 8, 10]
.map(x => x * x); // [4, 16, 36, 64, 100]
// Transducer: single pass, no intermediate arrays
const mapping = (fn) => (reducer) => (acc, val) => reducer(acc, fn(val));
const filtering = (pred) => (reducer) => (acc, val) => pred(val) ? reducer(acc, val) : acc;
const xform = compose(
filtering(x => x % 2 === 0),
mapping(x => x * x),
);
const append = (arr, val) => [...arr, val];
const result2 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
.reduce(xform(append), []);
console.log(result2); // [4, 16, 36, 64, 100]
Pro Tips
Tip 1: Point-Free Style (Tacit Programming)
// Regular style: explicit arguments
const isEven = x => x % 2 === 0;
const evens = numbers => numbers.filter(x => isEven(x));
// Point-free style: expressed purely through function composition
const not = fn => x => !fn(x);
const isOdd = not(isEven);
// Direct passing
const getEvens = numbers => numbers.filter(isEven);
const getOdds = numbers => numbers.filter(isOdd);
console.log(getEvens([1,2,3,4,5])); // [2, 4]
console.log(getOdds([1,2,3,4,5])); // [1, 3, 5]
Tip 2: Maybe Monad Pattern — null-safe processing
class Maybe {
constructor(value) { this._value = value; }
static of(value) { return new Maybe(value); }
isNothing() { return this._value === null || this._value === undefined; }
map(fn) {
return this.isNothing() ? this : Maybe.of(fn(this._value));
}
getOrElse(defaultValue) {
return this.isNothing() ? defaultValue : this._value;
}
}
// Null-safe data access
const user = { profile: { address: { city: "Seoul" } } };
const city = Maybe.of(user)
.map(u => u.profile)
.map(p => p.address)
.map(a => a.city)
.getOrElse("Unknown");
console.log(city); // "Seoul"
const noCity = Maybe.of(null)
.map(u => u.profile)
.map(p => p.address)
.getOrElse("Unknown");
console.log(noCity); // "Unknown"
Tip 3: Functional state management
// Redux-style reducer — state transformation via pure functions
const initialState = { count: 0, history: [] };
const reducer = (state = initialState, action) => {
switch (action.type) {
case "INCREMENT":
return {
...state,
count: state.count + (action.payload ?? 1),
history: [...state.history, `+${action.payload ?? 1}`],
};
case "DECREMENT":
return {
...state,
count: state.count - (action.payload ?? 1),
history: [...state.history, `-${action.payload ?? 1}`],
};
case "RESET":
return { ...initialState };
default:
return state;
}
};
// Pure function — very easy to test
let state = reducer(undefined, { type: "INCREMENT" });
state = reducer(state, { type: "INCREMENT", payload: 5 });
state = reducer(state, { type: "DECREMENT", payload: 2 });
console.log(state.count); // 4
console.log(state.history); // ["+1", "+5", "-2"]