Functional Programming
Learn functional programming concepts in JavaScript — pure functions, immutability, currying, and function composition.
Functional Programming
Functional programming is a programming style that treats computation as the evaluation of mathematical functions — avoiding changing state and mutable data.
Instead of telling the computer how to do something step by step, you describe what you want. Instead of modifying data, you create new data. Instead of shared mutable state, you pass values through functions.
// Imperative — how to do it
const numbers = [1, 2, 3, 4, 5];
const result = [];
for (let i = 0; i < numbers.length; i++) {
if (numbers[i] % 2 === 0) {
result.push(numbers[i] * 2);
}
}
// Functional — what you want
const result = numbers
.filter(n => n % 2 === 0)
.map(n => n * 2);Same result. The functional version is shorter, more readable, and easier to reason about.
JavaScript is not a purely functional language — but it supports functional programming well. Functions are first-class values, arrays have map/filter/reduce, and the style fits naturally.
Core Concepts
Functional programming is built on a set of core ideas. You do not have to use all of them — but understanding them makes you a better JavaScript developer regardless of style.
1. Pure Functions
A pure function always returns the same output for the same input and has no side effects.
// ✅ Pure — same input always gives same output, no side effects
function add(a, b) {
return a + b;
}
function formatName(first, last) {
return `${first} ${last}`;
}
function double(numbers) {
return numbers.map(n => n * 2); // returns new array, does not modify input
}// ❌ Impure — depends on external state
let tax = 0.17;
function calculatePrice(price) {
return price + price * tax; // depends on external variable
}
// ❌ Impure — has side effects
function addUser(users, user) {
users.push(user); // modifies the input array
return users;
}
// ❌ Impure — produces different output each time
function getTimestamp() {
return Date.now(); // different every call
}Why pure functions matter
// Pure functions are:
// 1. Easy to test — no setup needed
console.log(add(2, 3) === 5); // always true
// 2. Easy to reason about — no hidden dependencies
// 3. Safe to run in parallel — no shared state
// 4. Cacheable — same input, same output, cache it
const memoized = memoize(expensiveCalculation);
// 5. Composable — chain them together
const result = compose(double, filterEvens, sort)(numbers);2. Immutability
Immutability means never changing data — creating new data instead.
// ❌ Mutable — modifies the original
function addItemMutable(cart, item) {
cart.items.push(item); // modifies original
cart.total += item.price; // modifies original
return cart;
}
// ✅ Immutable — creates new data
function addItemImmutable(cart, item) {
return {
...cart,
items: [...cart.items, item],
total: cart.total + item.price
};
}
const cart = { items: [], total: 0 };
const newCart = addItemImmutable(cart, { name: "Laptop", price: 120000 });
console.log(cart.items.length); // 0 — original unchanged
console.log(newCart.items.length); // 1 — new cart has the itemImmutable array operations
const numbers = [1, 2, 3, 4, 5];
// ❌ Mutating methods
numbers.push(6); // modifies original
numbers.pop(); // modifies original
numbers.splice(1, 1); // modifies original
// ✅ Immutable alternatives
const withSix = [...numbers, 6]; // add to end
const withoutLast = numbers.slice(0, -1); // remove last
const withoutIndex1 = [ // remove at index
...numbers.slice(0, 1),
...numbers.slice(2)
];
const updated = numbers.map((n, i) => // update at index
i === 2 ? 99 : n
);Immutable object operations
const user = { name: "Ali", age: 22, city: "Lahore" };
// ❌ Mutating
user.age = 23;
delete user.city;
// ✅ Immutable
const olderUser = { ...user, age: 23 };
const { city, ...userWithoutCity } = user;3. First-Class and Higher-Order Functions
In JavaScript, functions are first-class citizens — they can be assigned to variables, passed as arguments, and returned from other functions.
A higher-order function is a function that takes a function as an argument or returns a function.
// Functions stored in variables
const greet = name => `Hello, ${name}!`;
// Functions passed as arguments
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2); // map takes a function
const evens = numbers.filter(n => n % 2 === 0); // filter takes a function
// Functions returned from functions
function multiplier(factor) {
return n => n * factor; // returns a function
}
const triple = multiplier(3);
console.log(triple(5)); // 15Higher-order functions are what make functional programming powerful. map, filter, reduce, sort — all higher-order functions you already use.
4. Currying
Currying transforms a function that takes multiple arguments into a sequence of functions that each take one argument.
// Regular function
function add(a, b, c) {
return a + b + c;
}
console.log(add(1, 2, 3)); // 6
// Curried version
function curriedAdd(a) {
return function(b) {
return function(c) {
return a + b + c;
};
};
}
console.log(curriedAdd(1)(2)(3)); // 6
// With arrow functions — cleaner
const curriedAdd = a => b => c => a + b + c;
console.log(curriedAdd(1)(2)(3)); // 6Why curry?
Currying enables partial application — lock in some arguments now and supply the rest later.
const multiply = a => b => a * b;
const double = multiply(2); // a = 2, waiting for b
const triple = multiply(3); // a = 3, waiting for b
const tenX = multiply(10);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(tenX(5)); // 50
// Real use — reusable validators
const isGreaterThan = min => value => value > min;
const isLessThan = max => value => value < max;
const isAdult = isGreaterThan(17);
const isReasonableAge = isLessThan(150);
const ages = [15, 22, 8, 35, 200];
console.log(ages.filter(isAdult)); // [22, 35, 200]
console.log(ages.filter(isReasonableAge)); // [15, 22, 8, 35]A generic curry function
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function(...moreArgs) {
return curried.apply(this, args.concat(moreArgs));
};
};
}
function add(a, b, c) {
return a + b + c;
}
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6
console.log(curriedAdd(1, 2, 3)); // 65. Function Composition
Composition combines multiple functions into one — the output of one function becomes the input of the next.
// Manual composition
const result = format(sanitize(validate(input)));
// reads right to left — hard to follow
// compose — right to left
const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x);
const process = compose(format, sanitize, validate);
const result = process(input);
// pipe — left to right (more readable)
const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);
const process = pipe(validate, sanitize, format);
const result = process(input);Real example — text processing pipeline
const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);
// Individual transformation functions — each pure
const trim = str => str.trim();
const toLowerCase = str => str.toLowerCase();
const removeSpecialChars = str => str.replace(/[^a-z0-9\s-]/g, "");
const replaceSpaces = str => str.replace(/\s+/g, "-");
const removeDuplicateDashes = str => str.replace(/-+/g, "-");
// Compose into a slug generator
const createSlug = pipe(
trim,
toLowerCase,
removeSpecialChars,
replaceSpaces,
removeDuplicateDashes
);
console.log(createSlug(" Hello, World! ")); // hello-world
console.log(createSlug("JavaScript is AWESOME!")); // javascript-is-awesome
console.log(createSlug(" ES6+ Features & More ")); // es6-features-moreEach function does one thing. pipe connects them. Adding a new transformation is as simple as adding it to the pipeline.
6. Avoiding Side Effects
A side effect is anything a function does besides returning a value — modifying external state, logging, network requests, DOM manipulation.
Pure functions have no side effects. But real programs need side effects — you have to update the DOM, make API calls, save to storage. The functional approach is to isolate side effects rather than eliminate them.
// ❌ Side effects mixed with logic — hard to test
function processAndDisplay(users) {
const admins = users.filter(u => u.role === "admin"); // logic
document.getElementById("count").textContent = admins.length; // side effect
fetch("/api/log", { method: "POST", body: JSON.stringify(admins) }); // side effect
return admins;
}
// ✅ Separate pure logic from side effects
function getAdmins(users) {
return users.filter(u => u.role === "admin"); // pure — no side effects
}
// Side effects in one place — easy to find and test separately
function displayAdminCount(count) {
document.getElementById("count").textContent = count;
}
async function logAdmins(admins) {
await fetch("/api/log", { method: "POST", body: JSON.stringify(admins) });
}
// Combine at the edge
const admins = getAdmins(users); // pure
displayAdminCount(admins.length); // side effect — isolated
await logAdmins(admins); // side effect — isolated7. Functor and Map
A functor is any container that implements map — applying a function to the value inside without changing the container structure.
Arrays are functors. Promises are functors. Options/Maybe types are functors. You already use functors constantly.
// Array functor
[1, 2, 3].map(n => n * 2); // [2, 4, 6]
// Promise functor (sort of)
fetch("/api/user")
.then(res => res.json()) // transform the value inside
.then(user => user.name); // transform again
// Custom functor — a Box that holds a value
class Box {
constructor(value) {
this.value = value;
}
map(fn) {
return new Box(fn(this.value)); // apply fn, return new Box
}
fold(fn) {
return fn(this.value); // extract the value
}
}
const result = new Box(5)
.map(n => n * 2) // Box(10)
.map(n => n + 1) // Box(11)
.map(n => `Value: ${n}`) // Box("Value: 11")
.fold(s => s); // "Value: 11"
console.log(result); // "Value: 11"Putting It All Together — A Functional Data Pipeline
const pipe = (...fns) => x => fns.reduce((acc, fn) => fn(acc), x);
const curry = fn => {
const arity = fn.length;
return function curried(...args) {
if (args.length >= arity) return fn(...args);
return (...more) => curried(...args, ...more);
};
};
// Pure utility functions
const prop = curry((key, obj) => obj[key]);
const filter = curry((pred, arr) => arr.filter(pred));
const map = curry((fn, arr) => arr.map(fn));
const sortBy = curry((key, arr) => [...arr].sort((a, b) =>
a[key] > b[key] ? 1 : -1
));
// Data
const students = [
{ name: "Ali", score: 88, grade: "A", city: "Lahore" },
{ name: "Sara", score: 45, grade: "F", city: "Karachi" },
{ name: "Zara", score: 92, grade: "A", city: "Lahore" },
{ name: "Omar", score: 61, grade: "C", city: "Islamabad" },
{ name: "Hassan", score: 37, grade: "F", city: "Lahore" },
];
// Build pipelines from small pieces
const isPassing = student => student.score >= 50;
const getName = prop("name");
const getScore = prop("score");
const getPassingStudentNames = pipe(
filter(isPassing),
sortBy("score"),
map(getName)
);
const getAverageScore = students => {
const total = students.reduce((sum, s) => sum + s.score, 0);
return total / students.length;
};
const getLahorePassers = pipe(
filter(s => s.city === "Lahore"),
filter(isPassing),
map(getName)
);
console.log(getPassingStudentNames(students));
// ["Omar", "Ali", "Zara"]
console.log(getAverageScore(students));
// 64.6
console.log(getLahorePassers(students));
// ["Ali", "Zara"]Each function does one thing. Pipelines compose them. Adding a new requirement is adding one more step to a pipeline.
Summary
- Functional programming treats computation as function evaluation — avoiding shared state and mutation
- Pure functions — same input always gives same output, no side effects — easy to test and compose
- Immutability — never modify data, create new data instead — prevents accidental bugs
- Higher-order functions — functions that take or return other functions —
map,filter,reduce - Currying — transform multi-argument functions into chains of single-argument functions — enables partial application
- Composition — combine functions into pipelines —
pipe(left to right) andcompose(right to left) - Isolate side effects — separate pure logic from DOM manipulation, API calls, and logging
- You do not have to be purely functional — take the ideas that make your code clearer and more maintainable