Functional programming emphasizes immutability, pure functions, and composition. These patterns lead to code that's easier to test, debug, and reason about.
Core Concepts#
Pure Functions#
1// ❌ Impure - depends on external state
2let taxRate = 0.1;
3function calculateTax(amount) {
4 return amount * taxRate; // External dependency
5}
6
7// ❌ Impure - has side effects
8function addToCart(cart, item) {
9 cart.push(item); // Mutates input
10 return cart;
11}
12
13// ✅ Pure - same input always gives same output
14function calculateTax(amount, taxRate) {
15 return amount * taxRate;
16}
17
18// ✅ Pure - no mutation
19function addToCart(cart, item) {
20 return [...cart, item]; // Returns new array
21}Immutability#
1// ❌ Mutation
2const user = { name: 'John', age: 30 };
3user.age = 31; // Mutates original
4
5// ✅ Immutable update
6const user = { name: 'John', age: 30 };
7const updatedUser = { ...user, age: 31 }; // New object
8
9// ✅ Nested immutable update
10const state = {
11 user: { name: 'John', settings: { theme: 'dark' } }
12};
13
14const newState = {
15 ...state,
16 user: {
17 ...state.user,
18 settings: {
19 ...state.user.settings,
20 theme: 'light'
21 }
22 }
23};
24
25// Using Immer for complex updates
26import { produce } from 'immer';
27
28const newState = produce(state, draft => {
29 draft.user.settings.theme = 'light';
30});First-Class Functions#
1// Functions as values
2const add = (a, b) => a + b;
3const multiply = (a, b) => a * b;
4
5// Functions as arguments
6function calculate(operation, a, b) {
7 return operation(a, b);
8}
9
10calculate(add, 2, 3); // 5
11calculate(multiply, 2, 3); // 6
12
13// Functions as return values
14function createMultiplier(factor) {
15 return (number) => number * factor;
16}
17
18const double = createMultiplier(2);
19const triple = createMultiplier(3);
20
21double(5); // 10
22triple(5); // 15Higher-Order Functions#
Map, Filter, Reduce#
1const numbers = [1, 2, 3, 4, 5];
2
3// Map: transform each element
4const doubled = numbers.map(n => n * 2);
5// [2, 4, 6, 8, 10]
6
7// Filter: select elements
8const evens = numbers.filter(n => n % 2 === 0);
9// [2, 4]
10
11// Reduce: combine to single value
12const sum = numbers.reduce((acc, n) => acc + n, 0);
13// 15
14
15// Chaining
16const result = numbers
17 .filter(n => n % 2 === 0)
18 .map(n => n * 2)
19 .reduce((acc, n) => acc + n, 0);
20// 12 (2*2 + 4*2)Custom Higher-Order Functions#
1// Retry wrapper
2function withRetry(fn, maxRetries = 3) {
3 return async (...args) => {
4 let lastError;
5 for (let i = 0; i < maxRetries; i++) {
6 try {
7 return await fn(...args);
8 } catch (error) {
9 lastError = error;
10 await delay(Math.pow(2, i) * 1000);
11 }
12 }
13 throw lastError;
14 };
15}
16
17const fetchWithRetry = withRetry(fetch);
18await fetchWithRetry('https://api.example.com/data');
19
20// Memoization
21function memoize(fn) {
22 const cache = new Map();
23 return (...args) => {
24 const key = JSON.stringify(args);
25 if (cache.has(key)) {
26 return cache.get(key);
27 }
28 const result = fn(...args);
29 cache.set(key, result);
30 return result;
31 };
32}
33
34const expensiveCalculation = memoize((n) => {
35 console.log('Calculating...');
36 return n * n;
37});
38
39expensiveCalculation(5); // Calculating... 25
40expensiveCalculation(5); // 25 (cached)Function Composition#
Pipe and Compose#
1// Pipe: left to right
2const pipe = (...fns) => (value) =>
3 fns.reduce((acc, fn) => fn(acc), value);
4
5// Compose: right to left
6const compose = (...fns) => (value) =>
7 fns.reduceRight((acc, fn) => fn(acc), value);
8
9// Usage
10const addOne = x => x + 1;
11const double = x => x * 2;
12const square = x => x * x;
13
14const calculate = pipe(addOne, double, square);
15calculate(2); // ((2 + 1) * 2)² = 36
16
17const calculate2 = compose(square, double, addOne);
18calculate2(2); // Same result: 36Practical Composition#
1// Data transformation pipeline
2const processUser = pipe(
3 validateUser,
4 normalizeEmail,
5 hashPassword,
6 addTimestamps,
7 saveToDatabase
8);
9
10// String processing
11const slugify = pipe(
12 str => str.toLowerCase(),
13 str => str.trim(),
14 str => str.replace(/\s+/g, '-'),
15 str => str.replace(/[^a-z0-9-]/g, '')
16);
17
18slugify(' Hello World! '); // 'hello-world'Currying and Partial Application#
Currying#
1// Manual currying
2const add = a => b => c => a + b + c;
3add(1)(2)(3); // 6
4
5// Curry helper
6const curry = (fn) => {
7 const arity = fn.length;
8 return function curried(...args) {
9 if (args.length >= arity) {
10 return fn(...args);
11 }
12 return (...more) => curried(...args, ...more);
13 };
14};
15
16const curriedAdd = curry((a, b, c) => a + b + c);
17curriedAdd(1)(2)(3); // 6
18curriedAdd(1, 2)(3); // 6
19curriedAdd(1)(2, 3); // 6
20curriedAdd(1, 2, 3); // 6Partial Application#
1// Create specialized functions
2const multiply = (a, b) => a * b;
3const double = multiply.bind(null, 2);
4const triple = multiply.bind(null, 3);
5
6double(5); // 10
7triple(5); // 15
8
9// Partial application helper
10const partial = (fn, ...presetArgs) =>
11 (...laterArgs) => fn(...presetArgs, ...laterArgs);
12
13const greet = (greeting, name) => `${greeting}, ${name}!`;
14const sayHello = partial(greet, 'Hello');
15const sayGoodbye = partial(greet, 'Goodbye');
16
17sayHello('John'); // 'Hello, John!'
18sayGoodbye('John'); // 'Goodbye, John!'Functors and Monads#
Maybe (Handling Null)#
1class Maybe {
2 constructor(value) {
3 this.value = value;
4 }
5
6 static of(value) {
7 return new Maybe(value);
8 }
9
10 isNothing() {
11 return this.value === null || this.value === undefined;
12 }
13
14 map(fn) {
15 return this.isNothing() ? Maybe.of(null) : Maybe.of(fn(this.value));
16 }
17
18 flatMap(fn) {
19 return this.isNothing() ? Maybe.of(null) : fn(this.value);
20 }
21
22 getOrElse(defaultValue) {
23 return this.isNothing() ? defaultValue : this.value;
24 }
25}
26
27// Usage
28const user = { profile: { address: { city: 'NYC' } } };
29
30// Without Maybe (defensive coding)
31const city = user && user.profile && user.profile.address
32 ? user.profile.address.city
33 : 'Unknown';
34
35// With Maybe
36const city = Maybe.of(user)
37 .map(u => u.profile)
38 .map(p => p.address)
39 .map(a => a.city)
40 .getOrElse('Unknown');Either (Error Handling)#
1class Either {
2 constructor(value, isRight = true) {
3 this.value = value;
4 this.isRight = isRight;
5 }
6
7 static right(value) {
8 return new Either(value, true);
9 }
10
11 static left(value) {
12 return new Either(value, false);
13 }
14
15 map(fn) {
16 return this.isRight ? Either.right(fn(this.value)) : this;
17 }
18
19 flatMap(fn) {
20 return this.isRight ? fn(this.value) : this;
21 }
22
23 fold(leftFn, rightFn) {
24 return this.isRight ? rightFn(this.value) : leftFn(this.value);
25 }
26}
27
28// Usage
29function divide(a, b) {
30 return b === 0
31 ? Either.left('Cannot divide by zero')
32 : Either.right(a / b);
33}
34
35divide(10, 2)
36 .map(result => result * 2)
37 .fold(
38 error => console.error(error),
39 result => console.log(result) // 10
40 );
41
42divide(10, 0)
43 .map(result => result * 2) // Skipped
44 .fold(
45 error => console.error(error), // 'Cannot divide by zero'
46 result => console.log(result)
47 );Practical Patterns#
Point-Free Style#
1// With points (explicit parameters)
2const getNames = users => users.map(user => user.name);
3
4// Point-free (no explicit parameters)
5const prop = key => obj => obj[key];
6const getNames = users => users.map(prop('name'));
7
8// Or with composition
9const getName = prop('name');
10const getNames = map(getName);Pipeline Operator (Proposal)#
1// Future JavaScript (stage 2 proposal)
2const result = value
3 |> addOne
4 |> double
5 |> square;
6
7// Currently, use pipe function
8const result = pipe(addOne, double, square)(value);Conclusion#
Functional programming patterns make code more predictable and testable. Start with pure functions and immutability, then gradually adopt composition and higher-order functions.
You don't need to go full functional—even partial adoption improves code quality. Use these patterns where they make sense, and keep code readable.