Functional Programming
Pure functions, immutability, higher-order functions, currying, and composition
Functional Programming in JavaScript
Functional programming (FP) is a paradigm that treats computation as the evaluation of mathematical functions. It emphasizes immutability, pure functions, and declarative code. JavaScript supports FP patterns alongside object-oriented programming.
Core Principles
- Pure Functions — Same input always produces same output, no side effects
- Immutability — Data is never modified, only transformed into new data
- First-Class Functions — Functions are values that can be passed around
- Declarative — Describe what to do, not how to do it
Pure Functions
// ❌ Impure - modifies external state
let total = 0;
function addToTotal(value) {
total += value; // Side effect!
return total;
}
// ❌ Impure - depends on external state
function getDiscount(price) {
return price * currentDiscountRate; // External dependency
}
// ❌ Impure - non-deterministic
function getRandomGreeting(name) {
const greetings = ["Hi", "Hello", "Hey"];
const random = Math.floor(Math.random() * 3);
return `${greetings[random]}, ${name}`; // Different outputs!
}
// ✅ Pure - same input, same output, no side effects
function add(a, b) {
return a + b;
}
function getDiscount(price, discountRate) {
return price * discountRate;
}
function formatUser(user) {
return `${user.firstName} ${user.lastName}`;
}
Immutability
// ❌ Mutating data
const user = { name: "Alice", age: 30 };
user.age = 31; // Mutation!
const numbers = [1, 2, 3];
numbers.push(4); // Mutation!
// ✅ Creating new data
const updatedUser = { ...user, age: 31 };
const newNumbers = [...numbers, 4];
// Array methods that return new arrays
const doubled = numbers.map(n => n * 2); // [2, 4, 6]
const evens = numbers.filter(n => n % 2 === 0); // [2]
const sum = numbers.reduce((a, b) => a + b, 0); // 6
// Deep immutability with nested objects
const state = {
user: { name: "Alice", settings: { theme: "dark" } },
posts: [1, 2, 3]
};
// Update nested property immutably
const newState = {
...state,
user: {
...state.user,
settings: {
...state.user.settings,
theme: "light"
}
}
};
// Object.freeze for shallow immutability
const frozen = Object.freeze({ a: 1 });
frozen.a = 2; // Silently fails (throws in strict mode)
Higher-Order Functions
Functions that take functions as arguments or return functions:
// Array methods are higher-order functions
const users = [
{ name: "Alice", age: 30 },
{ name: "Bob", age: 25 },
{ name: "Charlie", age: 35 }
];
// map - transform each element
const names = users.map(user => user.name);
// ["Alice", "Bob", "Charlie"]
// filter - keep elements matching predicate
const adults = users.filter(user => user.age >= 30);
// [{ name: "Alice"... }, { name: "Charlie"... }]
// reduce - accumulate into single value
const totalAge = users.reduce((sum, user) => sum + user.age, 0);
// 90
// find - first matching element
const bob = users.find(user => user.name === "Bob");
// some / every - test predicates
const hasAdults = users.some(user => user.age >= 18); // true
const allAdults = users.every(user => user.age >= 18); // true
// Chaining
const result = users
.filter(u => u.age >= 30)
.map(u => u.name)
.sort();
// ["Alice", "Charlie"]
Function Composition
// Combine small functions into larger ones
const add10 = x => x + 10;
const multiply2 = x => x * 2;
const subtract5 = x => x - 5;
// Manual composition
const result = subtract5(multiply2(add10(5))); // ((5+10)*2)-5 = 25
// Compose function (right to left)
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
const calculate = compose(subtract5, multiply2, add10);
calculate(5); // 25
// Pipe function (left to right) - more readable
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
const process = pipe(
add10, // 5 + 10 = 15
multiply2, // 15 * 2 = 30
subtract5 // 30 - 5 = 25
);
process(5); // 25
// Practical example
const processUsers = pipe(
users => users.filter(u => u.active),
users => users.map(u => u.email),
emails => emails.map(e => e.toLowerCase()),
emails => [...new Set(emails)] // unique
);
Currying
Transform a function with multiple arguments into a sequence of functions:
// Regular function
function add(a, b, c) {
return a + b + c;
}
add(1, 2, 3); // 6
// Curried version
function curriedAdd(a) {
return function(b) {
return function(c) {
return a + b + c;
};
};
}
curriedAdd(1)(2)(3); // 6
// Arrow function currying
const curriedAdd = a => b => c => a + b + c;
// Partial application
const add1 = curriedAdd(1);
const add1and2 = add1(2);
add1and2(3); // 6
// Practical use case
const multiply = a => b => a * b;
const double = multiply(2);
const triple = multiply(3);
[1, 2, 3].map(double); // [2, 4, 6]
[1, 2, 3].map(triple); // [3, 6, 9]
// Generic curry function
const curry = (fn) => {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return (...more) => curried(...args, ...more);
};
};
const curriedSum = curry((a, b, c) => a + b + c);
curriedSum(1)(2)(3); // 6
curriedSum(1, 2)(3); // 6
curriedSum(1)(2, 3); // 6
Useful FP Utilities
// Identity
const identity = x => x;
// Constant
const constant = x => () => x;
const always5 = constant(5);
always5(); // 5
// Tap (for debugging in pipelines)
const tap = fn => x => { fn(x); return x; };
pipe(
add10,
tap(console.log), // Log intermediate value
multiply2
)(5);
// Memoization
const memoize = fn => {
const cache = new Map();
return (...args) => {
const key = JSON.stringify(args);
if (!cache.has(key)) {
cache.set(key, fn(...args));
}
return cache.get(key);
};
};
const expensiveFn = memoize((n) => {
console.log("Computing...");
return n * n;
});
expensiveFn(5); // "Computing..." → 25
expensiveFn(5); // 25 (from cache)
// Partial application
const partial = (fn, ...args) => (...more) => fn(...args, ...more);
const greet = (greeting, name) => `${greeting}, ${name}!`;
const sayHello = partial(greet, "Hello");
sayHello("Alice"); // "Hello, Alice!"
📚 Learn More
💡 Key Takeaways
- • Pure functions are predictable, testable, and cacheable
- • Prefer immutable updates with spread operator and array methods
- • Use map, filter, reduce for data transformations
- • Compose small functions into larger pipelines
- • Currying enables partial application and reusable functions
- • FP makes code more declarative and easier to reason about