Course: JavaScript Advanced
Lesson 8: Functional Programming Basics
Level
Advanced
Estimated Reading Time
10–15 minutes
Goal
By the end of this lesson, the learner will be able to:
- Write pure functions and explain why immutability makes code more predictable
- Use higher-order functions and function composition to build reusable logic pipelines
- Apply partial application and currying to create specialized functions from general ones
Frontend Development Context
Functional programming concepts have moved from academic curiosity to practical necessity in modern frontend development. React's core model — pure components, immutable state updates, one-way data flow — is built on functional programming principles. Libraries like Ramda, Lodash/fp, and RxJS adopt these concepts, and frameworks like Redux require reducers to be pure functions. Even without these tools, writing code in a functional style leads to logic that is more testable, more composable, and easier to reason about.
At the advanced level, understanding pure functions, higher-order functions, and function composition means you can build powerful abstractions that feel like building blocks rather than one-off scripts. Partial application and currying let you create specialized versions of general functions, which is a pattern used extensively in middleware, event systems, and component factories in production applications.
Explanation
A pure function is a function that, given the same inputs, always returns the same output and produces no side effects. It does not modify external state, mutate its arguments, or perform I/O. Pure functions are the foundation of functional programming because they are predictable, easy to test (no setup or teardown needed), and safe to memoize.
Immutability complements pure functions. Instead of mutating an object or array, you create a new one with the changes applied. JavaScript's spread operator, Object.assign, Array.prototype.map, and Array.prototype.filter all support immutable update patterns. This is why React state updates produce new objects rather than mutating existing ones — React relies on reference equality checks to detect changes.
Higher-order functions are functions that take other functions as arguments, return a function, or both. map, filter, and reduce are built-in higher-order functions. Writing your own higher-order functions lets you abstract over behavior — instead of repeating try/catch in every API call, write a withErrorHandling(fn) wrapper once. Instead of always formatting the same way, write a withLogging(fn) decorator.
Function composition means combining two or more functions where the output of one becomes the input of the next. Mathematically: compose(f, g)(x) === f(g(x)). In code, this allows you to build data transformation pipelines from small, focused functions. A compose utility runs functions right-to-left; a pipe utility runs them left-to-right (which reads more naturally in English).
Partial application is the process of fixing some arguments of a function and returning a new function that accepts the remaining arguments. Currying transforms a multi-argument function into a series of single-argument functions. Both techniques let you specialize general functions — create a multiplyBy3 from multiply(a, b) by fixing a = 3, for example. This pattern powers middleware chains, event handler factories, and configurable React component props.
Code Example
This example demonstrates pure functions, a compose/pipe utility, higher-order function decorators, and partial application working together.
// Pure functions — no side effects, same input always same output
const add = (a, b) => a + b;
const multiply = (a, b) => a * b;
const clamp = (min, max) => value => Math.min(Math.max(value, min), max);
// Partial application — fix one argument, return a specialized function
const double = multiply.bind(null, 2);
const triple = multiply.bind(null, 3);
const clampScore = clamp(0, 100);
console.log(double(7)); // 14
console.log(triple(7)); // 21
console.log(clampScore(115)); // 100
console.log(clampScore(-5)); // 0
// pipe — left-to-right function composition
const pipe = (...fns) => x => fns.reduce((v, fn) => fn(v), x);
const normalizeScore = pipe(
score => score * 1.1, // apply bonus
Math.round, // round to integer
clampScore, // clamp to 0–100
);
console.log(normalizeScore(88)); // 97 (88 * 1.1 = 96.8, rounded = 97, clamped = 97)
console.log(normalizeScore(95)); // 100 (95 * 1.1 = 104.5, rounded = 105, clamped = 100)
// Higher-order function — decorator that adds logging
function withLogging(fn, label) {
return function (...args) {
console.log(`[${label}] called with:`, args);
const result = fn(...args);
console.log(`[${label}] returned:`, result);
return result;
};
}
const loggedNormalize = withLogging(normalizeScore, "normalizeScore");
loggedNormalize(72);
// [normalizeScore] called with: [72]
// [normalizeScore] returned: 79
// Immutable update — never mutate, always return new structures
function applyBonus(user, bonusPoints) {
return {
...user,
score: clampScore(user.score + bonusPoints),
history: [...user.history, { type: "bonus", points: bonusPoints }],
};
}
const user = { name: "Alex", score: 90, history: [] };
const updatedUser = applyBonus(user, 15);
console.log(user.score); // 90 — original untouched
console.log(updatedUser.score); // 100 — clamped new copy
Code Explanation
add, multiply, and clamp are pure functions. They take inputs and return outputs with no external effects. clamp is a curried function — it takes min and max and returns a new function that takes value. Calling clamp(0, 100) produces clampScore, a specialized function.
pipe is a higher-order function that accepts any number of functions and returns a new function. When the returned function is called with a value, reduce threads that value through each function in order. normalizeScore is built by composing three steps: apply a bonus multiplier, round, and clamp. Each step is a pure, reusable function. The pipeline is readable as a description of the transformation.
withLogging is a decorator higher-order function. It wraps any function and returns a new version that logs before and after each call. The wrapped function's behavior is unchanged — logging is added without modifying the original. This is the open/closed principle in functional style: extend behavior without modification.
applyBonus demonstrates immutable updates. Instead of mutating user.score, it creates a new object with spread syntax, applying the transformation to the score field and appending to a new history array. The original user is completely untouched. This pattern is exactly how Redux reducers and React state updates must work.
Pattern Highlights
✅ Positive Pattern: Build transformation pipelines with
pipeorcomposefrom small, named, pure functions — each function is independently testable, and the pipeline reads as a clear description of what happens to the data.
⚠️ Neutral Note: Full functional programming with immutability everywhere can feel verbose in simple cases. Apply it where data flow, testability, or multiple transformations justify the overhead — not every function needs to be pure, but shared state and mutation should be deliberate and contained.
❌ Negative Pattern: Do not mutate function arguments — functions that modify their input objects create hidden coupling between callers and the function body, making bugs caused by shared state very hard to trace.
Common Mistakes
Mistake 1: Writing an impure function that modifies its argument
Mutating an array or object argument changes the caller's data without any visible indication.
function addItem(cart, item) {
cart.items.push(item); // mutates the caller's array
return cart;
}
const myCart = { items: ["apple"] };
addItem(myCart, "banana");
console.log(myCart.items); // ["apple", "banana"] — mutated!
Return a new object instead: return { ...cart, items: [...cart.items, item] }. The caller's original data is preserved.
Mistake 2: Composing functions with mismatched types
Composing functions that don't pass matching types between them produces silent errors or NaN.
const processPrice = pipe(
price => price * 1.2, // returns number
price => `$${price}`, // returns string
price => price * 1.1, // NaN — can't multiply a string
);
Each function in a composition pipeline must accept the type that the previous function returns. Keep each step's input/output contract consistent, and use explicit type conversions only at the boundaries.
Mistake 3: Treating every closure as a pure function
A function that reads from or writes to an external variable is not pure, even if it looks functional.
let taxRate = 0.2;
const calculateTotal = price => price * (1 + taxRate); // impure!
taxRate = 0.25;
console.log(calculateTotal(100)); // 125 — different result for same input
A pure version passes taxRate as a parameter: const calculateTotal = (price, rate) => price * (1 + rate). Now the function's output depends only on its inputs.
Quick Recap
- A pure function always returns the same output for the same input and has no side effects — it is the fundamental unit of functional programming and the easiest kind of function to test.
- Higher-order functions take functions as arguments or return functions, enabling decorators, middleware wrappers, and reusable behavior abstractions.
pipe(left-to-right) andcompose(right-to-left) chain pure functions into data transformation pipelines where each step does one thing.- Partial application and currying specialize general functions by pre-filling some arguments, producing focused tools that can be composed or reused in different contexts.