JavaScript — Functional Programming
Functional Programming (FP) is a paradigm that treats computation as the evaluation of mathematical functions and avoids changing state and mutable data. In JavaScript, FP is a first-class citizen — the language supports higher-order functions, closures, and anonymous functions, making it possible to write clean, declarative, and composable code.
FP's core tenets — pure functions, immutability, and declarative style — lead to code that is easier to reason about, test, and debug. When a function always produces the same output for the same input and has no side effects, you can understand it in isolation. When data is immutable, you never have to worry about unexpected mutations from other parts of your program.
JavaScript does not enforce FP — you can choose which concepts to adopt and to what degree. Many developers find the sweet spot is a hybrid approach: using pure functions and immutability for business logic while allowing pragmatic side effects at the system boundaries (I/O, DOM updates, network requests).
A pure function has two properties: given the same input, it always returns the same output; and it has no side effects (it does not modify external state, perform I/O, or depend on mutable external values). Pure functions are the building blocks of functional programming.
| Property | Pure Function | Impure Function |
|---|---|---|
| Deterministic | Same input → same output, always | May depend on Date, Math.random, global state |
| Side effects | None — no mutations, I/O, DOM, or logging | Modifies arguments, global state, or performs I/O |
| Testability | Trivial — no mocking needed | Requires mocks, setup, and cleanup |
| Caching | Safe to memoize (referential transparency) | Cannot cache reliably |
| 1 | // Pure function — deterministic, no side effects |
| 2 | function add(a, b) { |
| 3 | return a + b; |
| 4 | } |
| 5 | |
| 6 | function calculateTotal(items) { |
| 7 | return items.reduce((sum, item) => sum + item.price, 0); |
| 8 | } |
| 9 | |
| 10 | function toUpperCase(str) { |
| 11 | return str.toUpperCase(); |
| 12 | } |
| 13 | |
| 14 | // Impure functions |
| 15 | let counter = 0; |
| 16 | function incrementCounter() { |
| 17 | counter++; // Side effect: modifies external state |
| 18 | return counter; |
| 19 | } |
| 20 | |
| 21 | function getRandomDiscount(price) { |
| 22 | return price * Math.random(); // Not deterministic |
| 23 | } |
| 24 | |
| 25 | function formatTimestamp() { |
| 26 | return new Date().toISOString(); // Depends on external state (Date.now) |
| 27 | } |
| 28 | |
| 29 | // Idempotence — applying the operation multiple times |
| 30 | // has the same effect as applying it once |
| 31 | // HTTP example: PUT /users/1 { name: "Alice" } |
| 32 | // Calling it once vs. ten times has the same result |
| 33 | |
| 34 | // Math.abs is idempotent |
| 35 | console.log(Math.abs(-5)); // 5 |
| 36 | console.log(Math.abs(5)); // 5 |
| 37 | |
| 38 | // Delete is idempotent |
| 39 | function removeById(items, id) { |
| 40 | return items.filter(item => item.id !== id); |
| 41 | } |
| 42 | |
| 43 | // Pure function with object spread (immutable update) |
| 44 | function updateUserName(user, newName) { |
| 45 | return { ...user, name: newName }; |
| 46 | } |
best practice
Immutability means data never changes after creation. Instead of modifying an existing object or array, you create a new copy with the desired changes. This eliminates an entire class of bugs caused by unexpected mutations and makes change tracking trivial (for undo, time-travel debugging, or change detection).
| 1 | // Mutable approach (avoid in FP) |
| 2 | const user = { name: 'Alice', role: 'admin' }; |
| 3 | user.role = 'user'; // Mutation! |
| 4 | user.name = 'Bob'; // Mutation! |
| 5 | |
| 6 | // Immutable approach — create new objects |
| 7 | const original = { name: 'Alice', role: 'admin' }; |
| 8 | const updated = { ...original, role: 'user' }; |
| 9 | console.log(original); // { name: 'Alice', role: 'admin' } — unchanged |
| 10 | console.log(updated); // { name: 'Alice', role: 'user' } |
| 11 | |
| 12 | // Immutable arrays |
| 13 | const numbers = [1, 2, 3, 4, 5]; |
| 14 | |
| 15 | // Add — spread (prepend or append) |
| 16 | const withNew = [0, ...numbers]; // [0, 1, 2, 3, 4, 5] |
| 17 | const withEnd = [...numbers, 6]; // [1, 2, 3, 4, 5, 6] |
| 18 | |
| 19 | // Remove — filter |
| 20 | const without3 = numbers.filter(n => n !== 3); |
| 21 | |
| 22 | // Update — map |
| 23 | const doubled = numbers.map(n => n * 2); |
| 24 | |
| 25 | // Replace specific index |
| 26 | const replaceAtIndex = (arr, index, value) => [ |
| 27 | ...arr.slice(0, index), |
| 28 | value, |
| 29 | ...arr.slice(index + 1) |
| 30 | ]; |
| 31 | |
| 32 | // Nested updates (deep immutability) |
| 33 | const state = { |
| 34 | user: { name: 'Alice', settings: { theme: 'dark', lang: 'en' } } |
| 35 | }; |
| 36 | |
| 37 | // Update nested property |
| 38 | const newState = { |
| 39 | ...state, |
| 40 | user: { |
| 41 | ...state.user, |
| 42 | settings: { |
| 43 | ...state.user.settings, |
| 44 | theme: 'light' |
| 45 | } |
| 46 | } |
| 47 | }; |
| 48 | |
| 49 | // Alternative: immer-style update (simpler syntax) |
| 50 | function updateSetting(state, key, value) { |
| 51 | return { |
| 52 | ...state, |
| 53 | user: { |
| 54 | ...state.user, |
| 55 | settings: { |
| 56 | ...state.user.settings, |
| 57 | [key]: value |
| 58 | } |
| 59 | } |
| 60 | }; |
| 61 | } |
pro tip
A higher-order function is a function that takes one or more functions as arguments, returns a function, or both. This is possible because JavaScript treats functions as first-class citizens — they can be assigned to variables, passed as arguments, and returned from other functions.
| 1 | // HOF — function that takes a function |
| 2 | function withLogging(fn) { |
| 3 | return function(...args) { |
| 4 | console.log(`Calling ${fn.name || 'anonymous'}`); |
| 5 | return fn(...args); |
| 6 | }; |
| 7 | } |
| 8 | |
| 9 | const add = (a, b) => a + b; |
| 10 | const loggedAdd = withLogging(add); |
| 11 | console.log(loggedAdd(2, 3)); // "Calling add" then 5 |
| 12 | |
| 13 | // HOF — function returning a function |
| 14 | function multiply(factor) { |
| 15 | return function(x) { |
| 16 | return x * factor; |
| 17 | }; |
| 18 | } |
| 19 | |
| 20 | const double = multiply(2); |
| 21 | const triple = multiply(3); |
| 22 | console.log(double(5)); // 10 |
| 23 | console.log(triple(5)); // 15 |
| 24 | |
| 25 | // Array methods as HOFs |
| 26 | const numbers = [1, 2, 3, 4, 5]; |
| 27 | |
| 28 | // map — transform each element |
| 29 | const squared = numbers.map(n => n ** 2); |
| 30 | |
| 31 | // filter — keep elements matching predicate |
| 32 | const evens = numbers.filter(n => n % 2 === 0); |
| 33 | |
| 34 | // reduce — accumulate values |
| 35 | const sum = numbers.reduce((acc, n) => acc + n, 0); |
| 36 | |
| 37 | // find — get first match |
| 38 | const firstLarge = numbers.find(n => n > 3); |
| 39 | |
| 40 | // some / every — test elements |
| 41 | const hasEven = numbers.some(n => n % 2 === 0); |
| 42 | const allPositive = numbers.every(n => n > 0); |
| 43 | |
| 44 | // Custom HOF — create predicate factory |
| 45 | const greaterThan = (threshold) => (value) => value > threshold; |
| 46 | const isAdult = greaterThan(18); |
| 47 | console.log(isAdult(21)); // true |
| 48 | console.log(isAdult(15)); // false |
| 49 | |
| 50 | // Custom HOF — pluck property |
| 51 | const pluck = (key) => (obj) => obj[key]; |
| 52 | const getName = pluck('name'); |
| 53 | const users = [{ name: 'Alice' }, { name: 'Bob' }]; |
| 54 | console.log(users.map(getName)); // ['Alice', 'Bob'] |
info
Function composition combines two or more functions to produce a new function. The output of one function becomes the input of the next. This is the essence of functional programming — building complex operations by composing simple, pure functions.
| 1 | // Manual composition |
| 2 | const double = (x) => x * 2; |
| 3 | const increment = (x) => x + 1; |
| 4 | const toString = (x) => String(x); |
| 5 | |
| 6 | const result = toString(increment(double(5))); |
| 7 | console.log(result); // "11" |
| 8 | |
| 9 | // compose — right-to-left |
| 10 | function compose(...fns) { |
| 11 | return function(initial) { |
| 12 | return fns.reduceRight((acc, fn) => fn(acc), initial); |
| 13 | }; |
| 14 | } |
| 15 | |
| 16 | const doubleThenIncrement = compose(increment, double); |
| 17 | console.log(doubleThenIncrement(5)); // 11 |
| 18 | |
| 19 | // pipe — left-to-right (more intuitive order) |
| 20 | function pipe(...fns) { |
| 21 | return function(initial) { |
| 22 | return fns.reduce((acc, fn) => fn(acc), initial); |
| 23 | }; |
| 24 | } |
| 25 | |
| 26 | const processNumber = pipe(double, increment, toString); |
| 27 | console.log(processNumber(5)); // "11" |
| 28 | |
| 29 | // Real-world composition: data processing pipeline |
| 30 | const users = [ |
| 31 | { name: 'Alice', age: 30, active: true }, |
| 32 | { name: 'Bob', age: 17, active: true }, |
| 33 | { name: 'Charlie', age: 25, active: false }, |
| 34 | { name: 'Diana', age: 22, active: true }, |
| 35 | ]; |
| 36 | |
| 37 | // Define small reusable functions |
| 38 | const filterActive = (users) => users.filter(u => u.active); |
| 39 | const filterAdults = (users) => users.filter(u => u.age >= 18); |
| 40 | const pluckNames = (users) => users.map(u => u.name); |
| 41 | const sortAsc = (arr) => [...arr].sort(); |
| 42 | |
| 43 | // Compose them |
| 44 | const getActiveAdultNames = pipe(filterActive, filterAdults, pluckNames, sortAsc); |
| 45 | console.log(getActiveAdultNames(users)); // ['Alice', 'Diana'] |
| 46 | |
| 47 | // Each function is small, testable, and reusable |
| 48 | // The pipeline reads like a sentence: filterActive -> filterAdults -> pluckNames -> sort |
pro tip
Currying transforms a function that takes multiple arguments into a sequence of functions, each taking a single argument. Partial application pre-fills some arguments of a function, producing a function with fewer parameters. Both techniques create specialized functions from general ones.
| 1 | // Uncurried function |
| 2 | function add(a, b, c) { |
| 3 | return a + b + c; |
| 4 | } |
| 5 | |
| 6 | // Curried version |
| 7 | function curryAdd(a) { |
| 8 | return function(b) { |
| 9 | return function(c) { |
| 10 | return a + b + c; |
| 11 | }; |
| 12 | }; |
| 13 | } |
| 14 | |
| 15 | console.log(curryAdd(1)(2)(3)); // 6 |
| 16 | |
| 17 | // Arrow function curried |
| 18 | const curriedAdd = (a) => (b) => (c) => a + b + c; |
| 19 | |
| 20 | // Generic curry function |
| 21 | function curry(fn) { |
| 22 | return function curried(...args) { |
| 23 | if (args.length >= fn.length) { |
| 24 | return fn.apply(this, args); |
| 25 | } |
| 26 | return function(...more) { |
| 27 | return curried.apply(this, args.concat(more)); |
| 28 | }; |
| 29 | }; |
| 30 | } |
| 31 | |
| 32 | const curriedMultiply = curry((a, b, c) => a * b * c); |
| 33 | console.log(curriedMultiply(2)(3)(4)); // 24 |
| 34 | console.log(curriedMultiply(2, 3)(4)); // 24 |
| 35 | console.log(curriedMultiply(2, 3, 4)); // 24 |
| 36 | |
| 37 | // Partial application |
| 38 | function partial(fn, ...presetArgs) { |
| 39 | return function(...laterArgs) { |
| 40 | return fn.apply(this, [...presetArgs, ...laterArgs]); |
| 41 | }; |
| 42 | } |
| 43 | |
| 44 | const add = (a, b, c) => a + b + c; |
| 45 | const add5 = partial(add, 5); // Partially apply first argument |
| 46 | const add5And10 = partial(add, 5, 10); // Partially apply two arguments |
| 47 | |
| 48 | console.log(add5(10, 15)); // 5 + 10 + 15 = 30 |
| 49 | console.log(add5And10(20)); // 5 + 10 + 20 = 35 |
| 50 | |
| 51 | // Practical: creating specialized HTTP functions |
| 52 | const fetchWithBase = partial(fetch, 'https://api.example.com'); |
| 53 | const getJSON = (url) => fetchWithBase(url).then(r => r.json()); |
| 54 | |
| 55 | // Practical: logger with preset level |
| 56 | const createLogger = (level) => (message) => |
| 57 | console.log(`[${level.toUpperCase()}] ${message}`); |
| 58 | |
| 59 | const info = createLogger('info'); |
| 60 | const warn = createLogger('warn'); |
| 61 | const error = createLogger('error'); |
| 62 | |
| 63 | info('Server started'); // [INFO] Server started |
| 64 | warn('Memory high'); // [WARN] Memory high |
info
Recursion is a technique where a function calls itself to solve a smaller instance of the same problem. In functional programming, recursion replaces iterative loops (for, while) since those involve mutable counters. JavaScript engines implement tail call optimization (TCO) in strict mode, though support varies.
| 1 | // Basic recursion — factorial |
| 2 | function factorial(n) { |
| 3 | if (n <= 1) return 1; // Base case |
| 4 | return n * factorial(n - 1); // Recursive case |
| 5 | } |
| 6 | |
| 7 | // Recursion — array sum |
| 8 | function sum(arr) { |
| 9 | if (arr.length === 0) return 0; |
| 10 | return arr[0] + sum(arr.slice(1)); |
| 11 | } |
| 12 | |
| 13 | // Tail-recursive version (optimized with TCO in strict mode) |
| 14 | function sumTail(arr, acc = 0) { |
| 15 | if (arr.length === 0) return acc; |
| 16 | return sumTail(arr.slice(1), acc + arr[0]); |
| 17 | } |
| 18 | |
| 19 | // Recursion — tree traversal |
| 20 | const tree = { |
| 21 | value: 1, |
| 22 | children: [ |
| 23 | { value: 2, children: [] }, |
| 24 | { |
| 25 | value: 3, |
| 26 | children: [ |
| 27 | { value: 4, children: [] }, |
| 28 | { value: 5, children: [] } |
| 29 | ] |
| 30 | } |
| 31 | ] |
| 32 | }; |
| 33 | |
| 34 | function deepFlatten(node) { |
| 35 | let values = [node.value]; |
| 36 | for (const child of node.children) { |
| 37 | values = values.concat(deepFlatten(child)); |
| 38 | } |
| 39 | return values; |
| 40 | } |
| 41 | |
| 42 | console.log(deepFlatten(tree)); // [1, 2, 3, 4, 5] |
| 43 | |
| 44 | // Recursion — deep object search |
| 45 | function findInTree(node, predicate) { |
| 46 | if (predicate(node)) return node; |
| 47 | for (const child of node.children) { |
| 48 | const found = findInTree(child, predicate); |
| 49 | if (found) return found; |
| 50 | } |
| 51 | return null; |
| 52 | } |
| 53 | |
| 54 | // Recursion — DOM traversal |
| 55 | function walkDOM(node, callback) { |
| 56 | callback(node); |
| 57 | node = node.firstChild; |
| 58 | while (node) { |
| 59 | walkDOM(node, callback); |
| 60 | node = node.nextSibling; |
| 61 | } |
| 62 | } |
| 63 | |
| 64 | walkDOM(document.body, (el) => { |
| 65 | if (el.nodeType === 1) { |
| 66 | console.log(el.tagName); |
| 67 | } |
| 68 | }); |
| 69 | |
| 70 | // Mutual recursion — even/odd check (demonstration) |
| 71 | function isEven(n) { |
| 72 | if (n === 0) return true; |
| 73 | return isOdd(n - 1); |
| 74 | } |
| 75 | |
| 76 | function isOdd(n) { |
| 77 | if (n === 0) return false; |
| 78 | return isEven(n - 1); |
| 79 | } |
| 80 | |
| 81 | console.log(isEven(4)); // true |
| 82 | console.log(isOdd(7)); // true |
warning
Functors and Monads are abstractions from category theory that have practical applications in JavaScript. A Functor is a container that implements a map method — you can transform the contained value without leaving the container. A Monad is a Functor that also implements flatMap (or chain), allowing you to avoid nested containers.
Maybe Monad
The Maybe monad represents an optional value that may or may not exist (avoiding null checks). It has two variants: Just (a value exists) and Nothing (no value). Operations on Nothing are safely ignored.
| 1 | // Maybe monad implementation |
| 2 | class Maybe { |
| 3 | static just(value) { return new Just(value); } |
| 4 | static nothing() { return new Nothing(); } |
| 5 | static of(value) { |
| 6 | return value == null ? Maybe.nothing() : Maybe.just(value); |
| 7 | } |
| 8 | |
| 9 | isNothing() { return false; } |
| 10 | isJust() { return false; } |
| 11 | } |
| 12 | |
| 13 | class Just extends Maybe { |
| 14 | constructor(value) { super(); this._value = value; } |
| 15 | |
| 16 | map(fn) { return Maybe.of(fn(this._value)); } |
| 17 | flatMap(fn) { return fn(this._value); } |
| 18 | getOrElse(defaultValue) { return this._value; } |
| 19 | get() { return this._value; } |
| 20 | } |
| 21 | |
| 22 | class Nothing extends Maybe { |
| 23 | map(fn) { return this; } |
| 24 | flatMap(fn) { return this; } |
| 25 | getOrElse(defaultValue) { return defaultValue; } |
| 26 | get() { throw new Error('Cannot get value from Nothing'); } |
| 27 | } |
| 28 | |
| 29 | // Usage — safe property access |
| 30 | function getConfigValue(config, path) { |
| 31 | return path.split('.').reduce( |
| 32 | (acc, key) => acc.flatMap(obj => Maybe.of(obj[key])), |
| 33 | Maybe.of(config) |
| 34 | ); |
| 35 | } |
| 36 | |
| 37 | const config = { |
| 38 | database: { host: 'localhost', port: 5432 } |
| 39 | }; |
| 40 | |
| 41 | const host = getConfigValue(config, 'database.host'); |
| 42 | console.log(host.getOrElse('default-host')); // localhost |
| 43 | |
| 44 | const missing = getConfigValue(config, 'database.password'); |
| 45 | console.log(missing.getOrElse('default-password')); // default-password |
| 46 | |
| 47 | // Safe computation pipeline |
| 48 | function safeParseJSON(str) { |
| 49 | try { |
| 50 | return Maybe.just(JSON.parse(str)); |
| 51 | } catch { |
| 52 | return Maybe.nothing(); |
| 53 | } |
| 54 | } |
| 55 | |
| 56 | function safeGetUser(id) { |
| 57 | // Simulated API call |
| 58 | const users = { 1: { name: 'Alice' } }; |
| 59 | return Maybe.of(users[id]); |
| 60 | } |
| 61 | |
| 62 | const result = safeGetUser(1) |
| 63 | .map(user => user.name) |
| 64 | .getOrElse('Unknown'); |
| 65 | console.log(result); // Alice |
| 66 | |
| 67 | // Either monad (success/failure with context) |
| 68 | class Either { |
| 69 | static left(value) { return new Left(value); } |
| 70 | static right(value) { return new Right(value); } |
| 71 | static of(value) { return Either.right(value); } |
| 72 | } |
| 73 | |
| 74 | class Left extends Either { |
| 75 | constructor(value) { super(); this._value = value; } |
| 76 | map(fn) { return this; } |
| 77 | flatMap(fn) { return this; } |
| 78 | getOrElse(defaultValue) { return defaultValue; } |
| 79 | get() { throw new Error(this._value); } |
| 80 | } |
| 81 | |
| 82 | class Right extends Either { |
| 83 | constructor(value) { super(); this._value = value; } |
| 84 | map(fn) { return Either.of(fn(this._value)); } |
| 85 | flatMap(fn) { return fn(this._value); } |
| 86 | getOrElse(defaultValue) { return this._value; } |
| 87 | get() { return this._value; } |
| 88 | } |
| 89 | |
| 90 | // Either for error handling |
| 91 | function divide(a, b) { |
| 92 | if (b === 0) return Either.left('Division by zero'); |
| 93 | return Either.right(a / b); |
| 94 | } |
| 95 | |
| 96 | const result1 = divide(10, 2) |
| 97 | .map(x => x * 2) |
| 98 | .getOrElse(0); |
| 99 | console.log(result1); // 10 |
| 100 | |
| 101 | const result2 = divide(10, 0) |
| 102 | .map(x => x * 2) |
| 103 | .getOrElse(0); |
| 104 | console.log(result2); // 0 (safe, no crash) |
note
Lazy Evaluation
Lazy evaluation delays computation until the result is actually needed. This enables working with infinite data structures and avoids unnecessary work. JavaScript is normally eager, but generators and thunks enable lazy patterns.
| 1 | // Lazy evaluation with generators |
| 2 | function* fibonacci() { |
| 3 | let a = 0, b = 1; |
| 4 | while (true) { |
| 5 | yield a; |
| 6 | [a, b] = [b, a + b]; |
| 7 | } |
| 8 | } |
| 9 | |
| 10 | const fib = fibonacci(); |
| 11 | console.log(fib.next().value); // 0 |
| 12 | console.log(fib.next().value); // 1 |
| 13 | console.log(fib.next().value); // 1 |
| 14 | console.log(fib.next().value); // 2 |
| 15 | console.log(fib.next().value); // 3 |
| 16 | // Values computed only when requested |
| 17 | |
| 18 | // Lazy range |
| 19 | function* range(start, end, step = 1) { |
| 20 | let current = start; |
| 21 | while (current < end) { |
| 22 | yield current; |
| 23 | current += step; |
| 24 | } |
| 25 | } |
| 26 | |
| 27 | // Lazy map |
| 28 | function* lazyMap(iterable, fn) { |
| 29 | for (const item of iterable) { |
| 30 | yield fn(item); |
| 31 | } |
| 32 | } |
| 33 | |
| 34 | // Lazy filter |
| 35 | function* lazyFilter(iterable, predicate) { |
| 36 | for (const item of iterable) { |
| 37 | if (predicate(item)) yield item; |
| 38 | } |
| 39 | } |
| 40 | |
| 41 | // Compose lazy operations — no intermediate arrays |
| 42 | const numbers = range(0, 1000000); |
| 43 | const evenSquares = lazyMap( |
| 44 | lazyFilter(numbers, n => n % 2 === 0), |
| 45 | n => n ** 2 |
| 46 | ); |
| 47 | |
| 48 | // Only the first 5 values are computed |
| 49 | for (const val of evenSquares) { |
| 50 | if (val > 100) break; |
| 51 | console.log(val); // 4, 16, 36, 64, 100 |
| 52 | } |
| 53 | |
| 54 | // Thunk — deferred computation |
| 55 | const expensive = () => { |
| 56 | console.log('Computing...'); |
| 57 | return 42; |
| 58 | }; |
| 59 | |
| 60 | const thunk = () => expensive(); // Nothing computed yet |
| 61 | console.log('Before'); // "Before" |
| 62 | const result = thunk(); // "Computing..." |
| 63 | console.log(result); // 42 |
| 64 | |
| 65 | // Memoized thunk (lazy with caching) |
| 66 | function lazy(fn) { |
| 67 | let evaluated = false; |
| 68 | let result; |
| 69 | return () => { |
| 70 | if (!evaluated) { |
| 71 | result = fn(); |
| 72 | evaluated = true; |
| 73 | } |
| 74 | return result; |
| 75 | }; |
| 76 | } |
| 77 | |
| 78 | const config = lazy(() => { |
| 79 | console.log('Loading config...'); |
| 80 | return JSON.parse(localStorage.getItem('config') || '{}'); |
| 81 | }); |
| 82 | |
| 83 | // Config loaded only on first access |
| 84 | console.log(config()); // "Loading config..." then {} |
| 85 | console.log(config()); // Cached, no loading |
Transducers
Transducers are composable, efficient transformations that work across different data types (arrays, streams, observables). They avoid creating intermediate collections by combining map/filter/reduce into a single pass.
| 1 | // The problem: intermediate arrays |
| 2 | const nums = [1, 2, 3, 4, 5, 6]; |
| 3 | const result = nums |
| 4 | .filter(n => n % 2 === 0) // [2, 4, 6] — intermediate |
| 5 | .map(n => n * 10) // [20, 40, 60] — intermediate |
| 6 | .slice(0, 2); // [20, 40] — final |
| 7 | |
| 8 | // Transducer approach: single pass |
| 9 | function mapping(fn) { |
| 10 | return (reducer) => (acc, value) => reducer(acc, fn(value)); |
| 11 | } |
| 12 | |
| 13 | function filtering(predicate) { |
| 14 | return (reducer) => (acc, value) => |
| 15 | predicate(value) ? reducer(acc, value) : acc; |
| 16 | } |
| 17 | |
| 18 | function taking(n) { |
| 19 | let count = 0; |
| 20 | return (reducer) => (acc, value) => { |
| 21 | if (count >= n) return acc; |
| 22 | count++; |
| 23 | return reducer(acc, value); |
| 24 | }; |
| 25 | } |
| 26 | |
| 27 | function compose(...fns) { |
| 28 | return fns.reduce((f, g) => (...args) => f(g(...args))); |
| 29 | } |
| 30 | |
| 31 | function transduce(transducer, reducer, initial, collection) { |
| 32 | const transformedReducer = transducer(reducer); |
| 33 | return collection.reduce(transformedReducer, initial); |
| 34 | } |
| 35 | |
| 36 | // Compose the transducer (applied right-to-left in compose) |
| 37 | const process = compose(taking(2), mapping(n => n * 10), filtering(n => n % 2 === 0)); |
| 38 | |
| 39 | const pushReducer = (acc, value) => { acc.push(value); return acc; }; |
| 40 | |
| 41 | const result2 = transduce(process, pushReducer, [], nums); |
| 42 | console.log(result2); // [20, 40] — single pass, no intermediates |
| 43 | |
| 44 | // Transducer works with any data structure |
| 45 | // Same transformation on a Set |
| 46 | const numSet = new Set([1, 2, 3, 4, 5, 6]); |
| 47 | const result3 = transduce(process, (acc, v) => acc.add(v), new Set(), numSet); |
| 48 | console.log(result3); // Set { 20, 40 } |
pro tip
Several libraries extend JavaScript's FP capabilities. Each takes a different approach — Ramda emphasizes point-free style and composition, lodash/fp provides auto-curried FP versions of lodash functions, and Immutable.js focuses on persistent data structures.
| Feature | Ramda | lodash/fp | Immutable.js | Native JS |
|---|---|---|---|---|
| Currying | Auto-curried, data-last | Auto-curried, data-last | N/A | Manual only |
| Composition | compose, pipe | flow, compose | N/A | Manual |
| Immutability | Shallow (functions return new refs) | Shallow (clone/mutate pattern) | Deep (persistent data structures) | Spread / Object.assign |
| Bundle size | ~15KB (tree-shakeable) | ~24KB (lodash full) | ~63KB | 0KB |
| Data structures | Plain arrays/objects | Plain arrays/objects | Map, List, Set, Record, Stack | Plain arrays/objects |
| Learning curve | Moderate (point-free style) | Low (lodash familiarity) | Moderate (new API) | None |
| 1 | // Ramda example (point-free, data-last) |
| 2 | import R from 'ramda'; |
| 3 | |
| 4 | const getActiveNames = R.pipe( |
| 5 | R.filter(R.propEq('active', true)), |
| 6 | R.map(R.prop('name')), |
| 7 | R.sortBy(R.identity) |
| 8 | ); |
| 9 | |
| 10 | // lodash/fp example (auto-curried, data-last) |
| 11 | import _ from 'lodash/fp'; |
| 12 | |
| 13 | const getActiveNamesFP = _.flow( |
| 14 | _.filter({ active: true }), |
| 15 | _.map('name'), |
| 16 | _.sortBy(_.identity) |
| 17 | ); |
| 18 | |
| 19 | // Immutable.js — persistent data structures |
| 20 | import { Map, List, fromJS } from 'immutable'; |
| 21 | |
| 22 | const state = fromJS({ |
| 23 | user: { name: 'Alice', settings: { theme: 'dark' } } |
| 24 | }); |
| 25 | |
| 26 | // Deep update without mutation |
| 27 | const newState = state.setIn(['user', 'settings', 'theme'], 'light'); |
| 28 | console.log(newState.getIn(['user', 'settings', 'theme'])); // light |
| 29 | console.log(state.getIn(['user', 'settings', 'theme'])); // dark (unchanged) |
| 30 | |
| 31 | // List operations |
| 32 | const list = List([1, 2, 3]); |
| 33 | const updated = list.push(4).map(n => n * 2); |
| 34 | console.log(updated.toJS()); // [2, 4, 6, 8] |
| 35 | |
| 36 | // Performance: structural sharing |
| 37 | // Immutable.js uses tries — only the changed path is copied |
| 38 | // The rest is shared with the original structure |
best practice
pro tip