JavaScript Functional Programming
1. Pure Functions and Side Effects Management
Pure Function Characteristics
| Characteristic | Description | Benefit |
|---|---|---|
| Deterministic | Same input → same output | Predictable, testable |
| No Side Effects | Doesn't modify external state | Safer, easier to reason about |
| No External Dependencies | Only uses parameters | Self-contained, portable |
| Referential Transparency | Can replace call with result | Optimizable, cacheable |
Common Side Effects
| Side Effect | Example | Impact |
|---|---|---|
| Mutation | Modifying objects/arrays | Unpredictable state changes |
| I/O Operations | Console.log, file read/write | Non-deterministic behavior |
| DOM Manipulation | Changing HTML elements | External state modification |
| Network Requests | API calls, fetch | Async, unpredictable timing |
| Global State | Accessing/modifying globals | Hidden dependencies |
| Random/Date | Math.random(), Date.now() | Non-deterministic output |
Managing Side Effects
| Strategy | Technique | Usage |
|---|---|---|
| Isolation | Keep side effects at boundaries | Pure core, impure shell |
| Dependency Injection | Pass dependencies as parameters | Explicit dependencies |
| Immutability | Never modify, create new | Prevent mutation side effects |
| Functional Core | Pure logic separate from I/O | Testable business logic |
Example: Pure functions vs impure
// IMPURE: Modifies external state
let total = 0;
function addToTotal(value) {
total += value; // Side effect: modifies global
return total;
}
console.log(addToTotal(5)); // 5
console.log(addToTotal(5)); // 10 (different output!)
// PURE: No side effects
function add(a, b) {
return a + b;
}
console.log(add(5, 5)); // 10
console.log(add(5, 5)); // 10 (always same output)
// IMPURE: Mutates input
function addItemImpure(array, item) {
array.push(item); // Mutates original array
return array;
}
const arr1 = [1, 2, 3];
addItemImpure(arr1, 4);
console.log(arr1); // [1, 2, 3, 4] - original modified!
// PURE: Returns new array
function addItemPure(array, item) {
return [...array, item]; // Creates new array
}
const arr2 = [1, 2, 3];
const arr3 = addItemPure(arr2, 4);
console.log(arr2); // [1, 2, 3] - original unchanged
console.log(arr3); // [1, 2, 3, 4] - new array
// IMPURE: Relies on external state
let discount = 0.1;
function calculatePriceImpure(price) {
return price * (1 - discount); // Uses external variable
}
// PURE: All dependencies explicit
function calculatePricePure(price, discount) {
return price * (1 - discount); // All inputs are parameters
}
console.log(calculatePricePure(100, 0.1)); // 90
// IMPURE: I/O operations
function logAndDouble(x) {
console.log(`Value: ${x}`); // Side effect: console output
return x * 2;
}
// PURE: Separate logic from I/O
function double(x) {
return x * 2;
}
function logValue(x) {
console.log(`Value: ${x}`);
}
// Use together
const result = double(5);
logValue(result);
// IMPURE: Non-deterministic
function getCurrentTimestamp() {
return Date.now(); // Different each call
}
// PURE: Pass time as parameter
function addHours(timestamp, hours) {
return timestamp + (hours * 60 * 60 * 1000);
}
// Managing side effects pattern
class UserService {
// Impure methods at boundaries
async getUser(id) {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
// Call pure function for transformation
return this.transformUserData(data);
}
// Pure function - easy to test
transformUserData(data) {
return {
id: data.id,
name: data.name.toUpperCase(),
age: data.age,
isAdult: data.age >= 18
};
}
}
// Pure core, impure shell pattern
// Pure core - business logic
const calculateOrderTotal = (items, taxRate) => {
const subtotal = items.reduce((sum, item) =>
sum + (item.price * item.quantity), 0
);
const tax = subtotal * taxRate;
return {
subtotal,
tax,
total: subtotal + tax
};
};
const validateOrder = (order) => {
if (!order.items || order.items.length === 0) {
return {valid: false, error: 'No items in order'};
}
if (order.items.some(item => item.price < 0)) {
return {valid: false, error: 'Invalid price'};
}
return {valid: true};
};
// Impure shell - handles I/O
const processOrder = async (orderId) => {
// Side effect: fetch from API
const order = await fetch(`/api/orders/${orderId}`).then(r => r.json());
// Pure: validate
const validation = validateOrder(order);
if (!validation.valid) {
throw new Error(validation.error);
}
// Pure: calculate
const total = calculateOrderTotal(order.items, 0.08);
// Side effect: save to database
await fetch(`/api/orders/${orderId}`, {
method: 'PUT',
body: JSON.stringify({...order, ...total})
});
// Side effect: send email
await sendOrderConfirmation(order.customerId, total);
return total;
};
// Dependency injection for testability
// Impure: hard to test
function getUserDataImpure(userId) {
const user = database.query(`SELECT * FROM users WHERE id = ${userId}`);
return user;
}
// Pure: inject dependencies
function getUserDataPure(userId, dbQuery) {
return dbQuery(`SELECT * FROM users WHERE id = ${userId}`);
}
// Usage
const result2 = getUserDataPure(123, database.query);
// Testing
const mockQuery = (sql) => ({id: 123, name: 'Test'});
const testResult = getUserDataPure(123, mockQuery);
// Memoization for pure functions
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn(...args);
cache.set(key, result);
return result;
};
}
// Works perfectly with pure functions
const expensivePureCalc = (n) => {
console.log('Calculating...');
return n * n;
};
const memoizedCalc = memoize(expensivePureCalc);
console.log(memoizedCalc(5)); // Calculating... 25
console.log(memoizedCalc(5)); // 25 (cached, no calculation)
Key Points: Pure functions have no side effects, return same
output for same input. Side effects include mutation, I/O, DOM manipulation, network requests. Isolate side
effects at program boundaries. Use dependency injection for testability. Pure functions are cacheable with
memoization. Separate pure core (business logic) from impure shell (I/O).
2. Higher-Order Functions and Function Composition
Higher-Order Function Types
| Type | Description | Example |
|---|---|---|
| Takes Function | Accepts function as parameter | map, filter, reduce |
| Returns Function | Returns new function | Factory functions, currying |
| Both | Takes and returns functions | Decorators, middleware |
Common Higher-Order Functions
| Function | Purpose | Returns |
|---|---|---|
| map(fn) | Transform each element | New array |
| filter(fn) | Select elements by predicate | Filtered array |
| reduce(fn, init) | Accumulate to single value | Accumulated value |
| forEach(fn) | Execute for each element | undefined |
| find(fn) | Find first matching element | Element or undefined |
| some(fn) | Test if any match | Boolean |
| every(fn) | Test if all match | Boolean |
Function Composition Patterns
| Pattern | Direction | Usage |
|---|---|---|
| compose | Right-to-left | compose(f, g, h)(x) = f(g(h(x))) |
| pipe | Left-to-right | pipe(f, g, h)(x) = h(g(f(x))) |
| chain | Method chaining | obj.method1().method2() |
Example: Higher-order functions
// Higher-order function: takes function as argument
function repeat(n, action) {
for (let i = 0; i < n; i++) {
action(i);
}
}
repeat(3, (i) => console.log(`Iteration ${i}`));
// Higher-order function: returns function
function greaterThan(n) {
return (m) => m > n;
}
const greaterThan10 = greaterThan(10);
console.log(greaterThan10(15)); // true
console.log(greaterThan10(5)); // false
// Array methods as higher-order functions
const numbers = [1, 2, 3, 4, 5];
// map: transform
const doubled = numbers.map(x => x * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
// filter: select
const evens = numbers.filter(x => x % 2 === 0);
console.log(evens); // [2, 4]
// reduce: accumulate
const sum = numbers.reduce((acc, x) => acc + x, 0);
console.log(sum); // 15
// Complex reduce example
const users = [
{name: 'John', age: 30, country: 'USA'},
{name: 'Jane', age: 25, country: 'UK'},
{name: 'Bob', age: 30, country: 'USA'}
];
const groupedByCountry = users.reduce((acc, user) => {
if (!acc[user.country]) {
acc[user.country] = [];
}
acc[user.country].push(user);
return acc;
}, {});
console.log(groupedByCountry);
// {USA: [{John}, {Bob}], UK: [{Jane}]}
// Creating custom higher-order functions
function filter(array, predicate) {
const result = [];
for (const item of array) {
if (predicate(item)) {
result.push(item);
}
}
return result;
}
function map(array, transform) {
const result = [];
for (const item of array) {
result.push(transform(item));
}
return result;
}
// Function factory
function multiplier(factor) {
return (number) => number * factor;
}
const double = multiplier(2);
const triple = multiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// Function that returns function with closure
function counter(start = 0) {
let count = start;
return {
increment: () => ++count,
decrement: () => --count,
value: () => count
};
}
const myCounter = counter(10);
console.log(myCounter.increment()); // 11
console.log(myCounter.increment()); // 12
console.log(myCounter.value()); // 12
// Decorator pattern with higher-order functions
function withLogging(fn) {
return function(...args) {
console.log(`Calling ${fn.name} with`, args);
const result = fn(...args);
console.log(`Result:`, result);
return result;
};
}
function withTiming(fn) {
return function(...args) {
const start = performance.now();
const result = fn(...args);
const end = performance.now();
console.log(`${fn.name} took ${(end - start).toFixed(2)}ms`);
return result;
};
}
function add2(a, b) {
return a + b;
}
const loggedAdd = withLogging(add2);
const timedAdd = withTiming(add2);
loggedAdd(2, 3);
timedAdd(2, 3);
// Stack decorators
const decoratedAdd = withLogging(withTiming(add2));
decoratedAdd(5, 10);
// Partial application helper
function partial(fn, ...fixedArgs) {
return function(...remainingArgs) {
return fn(...fixedArgs, ...remainingArgs);
};
}
function greet(greeting, name) {
return `${greeting}, ${name}!`;
}
const sayHello = partial(greet, 'Hello');
console.log(sayHello('John')); // Hello, John!
// Once function - execute only once
function once(fn) {
let called = false;
let result;
return function(...args) {
if (!called) {
called = true;
result = fn(...args);
}
return result;
};
}
const initialize = once(() => {
console.log('Initializing...');
return {initialized: true};
});
initialize(); // Initializing...
initialize(); // No output (already called)
Example: Function composition
// Compose: right-to-left execution
function compose(...fns) {
return function(value) {
return fns.reduceRight((acc, fn) => fn(acc), value);
};
}
// Pipe: left-to-right execution
function pipe(...fns) {
return function(value) {
return fns.reduce((acc, fn) => fn(acc), value);
};
}
// Example functions
const addOne = x => x + 1;
const double = x => x * 2;
const square = x => x * x;
// Using compose (right-to-left)
const composed = compose(square, double, addOne);
console.log(composed(3)); // square(double(addOne(3))) = square(8) = 64
// Using pipe (left-to-right)
const piped = pipe(addOne, double, square);
console.log(piped(3)); // square(double(addOne(3))) = square(8) = 64
// Real-world example: data transformation pipeline
const users2 = [
{name: 'john doe', age: 17, active: true},
{name: 'jane smith', age: 25, active: false},
{name: 'bob jones', age: 30, active: true}
];
// Individual transformation functions
const filterActive = users => users.filter(u => u.active);
const filterAdults = users => users.filter(u => u.age >= 18);
const capitalizeNames = users => users.map(u => ({
...u,
name: u.name.split(' ').map(word =>
word.charAt(0).toUpperCase() + word.slice(1)
).join(' ')
}));
const extractNames = users => users.map(u => u.name);
// Compose pipeline
const processUsers = pipe(
filterActive,
filterAdults,
capitalizeNames,
extractNames
);
console.log(processUsers(users2)); // ['Bob Jones']
// Point-free style (no explicit arguments)
const isEven = x => x % 2 === 0;
const increment = x => x + 1;
// Instead of: numbers.filter(x => isEven(x)).map(x => increment(x))
// Point-free:
const processNumbers = pipe(
arr => arr.filter(isEven),
arr => arr.map(increment)
);
console.log(processNumbers([1, 2, 3, 4, 5])); // [3, 5]
// Composable validation
const validateEmail = email => {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email) ? {valid: true} : {valid: false, error: 'Invalid email'};
};
const validateLength = (min, max) => str => {
return str.length >= min && str.length <= max
? {valid: true}
: {valid: false, error: `Length must be ${min}-${max}`};
};
const validatePassword = pipe(
validateLength(8, 20),
result => result.valid ?
(/[A-Z]/.test(result) ? {valid: true} : {valid: false, error: 'Need uppercase'}) :
result
);
// Composition with async functions
const asyncPipe = (...fns) => {
return async (value) => {
let result = value;
for (const fn of fns) {
result = await fn(result);
}
return result;
};
};
const fetchUser = async (id) => {
const response = await fetch(`/api/users/${id}`);
return response.json();
};
const enrichUser = async (user) => {
const posts = await fetch(`/api/users/${user.id}/posts`).then(r => r.json());
return {...user, posts};
};
const formatUser = (user) => ({
name: user.name.toUpperCase(),
postCount: user.posts.length
});
const getUserData = asyncPipe(
fetchUser,
enrichUser,
formatUser
);
// await getUserData(123);
// Transducer pattern (advanced composition)
const mapTransducer = (transform) => (reducer) => {
return (acc, value) => reducer(acc, transform(value));
};
const filterTransducer = (predicate) => (reducer) => {
return (acc, value) => predicate(value) ? reducer(acc, value) : acc;
};
const transduce = (transducer, reducer, initial, collection) => {
const xform = transducer(reducer);
return collection.reduce(xform, initial);
};
// Compose transducers
const composeTransducers = (...transducers) => {
return transducers.reduce((a, b) => (...args) => a(b(...args)));
};
// Usage
const xform = composeTransducers(
mapTransducer(x => x * 2),
filterTransducer(x => x > 5)
);
const result3 = transduce(
xform,
(acc, x) => acc.concat(x),
[],
[1, 2, 3, 4, 5]
);
console.log(result3); // [6, 8, 10]
Key Points: Higher-order functions accept functions as
arguments or return functions. Common examples: map, filter, reduce. Function
composition combines functions: compose (right-to-left), pipe (left-to-right). Decorators add behavior
by wrapping functions. Point-free style eliminates explicit arguments. Composition enables building complex
operations from simple functions.
3. Currying and Partial Application
Currying vs Partial Application
| Aspect | Currying | Partial Application |
|---|---|---|
| Definition | Transform f(a,b,c) to f(a)(b)(c) | Fix some arguments of function |
| Arguments | One argument at a time | Can fix multiple at once |
| Result | Always returns function | May return final result |
| Arity | Reduces to unary functions | Reduces arity by N |
Currying Benefits
| Benefit | Description | Use Case |
|---|---|---|
| Reusability | Create specialized functions | Configuration functions |
| Composition | Easier function composition | Building pipelines |
| Delayed Execution | Apply arguments over time | Event handlers, callbacks |
| Point-free Style | Write without explicit arguments | Cleaner code |
Partial Application Techniques
| Technique | Method | Example |
|---|---|---|
| bind | Function.prototype.bind | fn.bind(null, arg1, arg2) |
| Wrapper Function | Manual wrapping | (...args) => fn(fixed, ...args) |
| Helper Library | Lodash/Ramda partial | _.partial(fn, arg1) |
Example: Currying
// Manual currying
function add3(a, b, c) {
return a + b + c;
}
function curriedAdd(a) {
return function(b) {
return function(c) {
return a + b + c;
};
};
}
// Usage
console.log(curriedAdd(1)(2)(3)); // 6
// Arrow function currying
const curriedAdd2 = a => b => c => a + b + c;
console.log(curriedAdd2(1)(2)(3)); // 6
// Generic curry function
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...moreArgs) {
return curried.apply(this, args.concat(moreArgs));
};
}
};
}
// Usage
const add = (a, b, c) => a + b + c;
const curriedAddFunc = curry(add);
console.log(curriedAddFunc(1)(2)(3)); // 6
console.log(curriedAddFunc(1, 2)(3)); // 6
console.log(curriedAddFunc(1)(2, 3)); // 6
console.log(curriedAddFunc(1, 2, 3)); // 6
// Practical currying example: event handlers
const handleClick = curry((action, id, event) => {
event.preventDefault();
console.log(`Action: ${action}, ID: ${id}`);
});
// Create specialized handlers
const deleteHandler = handleClick('delete');
const editHandler = handleClick('edit');
// Use in event listeners
// element.addEventListener('click', deleteHandler(123));
// element.addEventListener('click', editHandler(456));
// Currying for configuration
const createLogger = level => prefix => message => {
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] [${level}] ${prefix}: ${message}`);
};
const infoLogger = createLogger('INFO');
const errorLogger = createLogger('ERROR');
const appInfoLogger = infoLogger('App');
const dbInfoLogger = infoLogger('Database');
appInfoLogger('Application started');
dbInfoLogger('Connected to database');
// Currying for API calls
const fetchFromAPI = curry((baseUrl, endpoint, params) => {
const url = new URL(endpoint, baseUrl);
Object.keys(params).forEach(key =>
url.searchParams.append(key, params[key])
);
return fetch(url.toString()).then(r => r.json());
});
const fetchFromMyAPI = fetchFromAPI('https://api.example.com');
const fetchUsers = fetchFromMyAPI('/users');
const fetchPosts = fetchFromMyAPI('/posts');
// await fetchUsers({page: 1, limit: 10});
// await fetchPosts({userId: 123});
// Currying for validation
const validate = curry((regex, message, value) => {
return regex.test(value)
? {valid: true}
: {valid: false, error: message};
});
const validateEmail = validate(
/^[^\s@]+@[^\s@]+\.[^\s@]+$/,
'Invalid email format'
);
const validatePhone = validate(
/^\d{3}-\d{3}-\d{4}$/,
'Invalid phone format'
);
console.log(validateEmail('user@example.com'));
console.log(validatePhone('555-123-4567'));
// Currying with multiple arguments preserved
const multiply = (a, b) => a * b;
const curriedMultiply = curry(multiply);
const double2 = curriedMultiply(2);
const triple2 = curriedMultiply(3);
console.log(double2(5)); // 10
console.log(triple2(5)); // 15
// Auto-currying with Proxy
function autoCurry(fn) {
return new Proxy(fn, {
apply(target, thisArg, args) {
if (args.length >= target.length) {
return target.apply(thisArg, args);
}
return autoCurry(target.bind(thisArg, ...args));
}
});
}
const sum = autoCurry((a, b, c, d) => a + b + c + d);
console.log(sum(1, 2, 3, 4)); // 10
console.log(sum(1)(2)(3)(4)); // 10
console.log(sum(1, 2)(3, 4)); // 10
console.log(sum(1)(2, 3)(4)); // 10
Example: Partial application
// Partial application with bind
function greet2(greeting, name) {
return `${greeting}, ${name}!`;
}
const sayHello2 = greet2.bind(null, 'Hello');
console.log(sayHello2('John')); // Hello, John!
const sayHi = greet2.bind(null, 'Hi');
console.log(sayHi('Jane')); // Hi, Jane!
// Custom partial function
function partial2(fn, ...fixedArgs) {
return function(...remainingArgs) {
return fn(...fixedArgs, ...remainingArgs);
};
}
function calculate(operation, a, b) {
switch(operation) {
case 'add': return a + b;
case 'subtract': return a - b;
case 'multiply': return a * b;
case 'divide': return a / b;
}
}
const add4 = partial2(calculate, 'add');
const subtract = partial2(calculate, 'subtract');
console.log(add4(5, 3)); // 8
console.log(subtract(10, 3)); // 7
// Partial from right (partial right)
function partialRight(fn, ...fixedArgs) {
return function(...remainingArgs) {
return fn(...remainingArgs, ...fixedArgs);
};
}
function divide(a, b) {
return a / b;
}
const divideBy2 = partialRight(divide, 2);
console.log(divideBy2(10)); // 5
// Placeholder support
const _ = Symbol('placeholder');
function partialWithPlaceholder(fn, ...fixedArgs) {
return function(...remainingArgs) {
const args = fixedArgs.map(arg =>
arg === _ ? remainingArgs.shift() : arg
);
return fn(...args, ...remainingArgs);
};
}
function formatDate(year, month, day) {
return `${year}-${month}-${day}`;
}
const formatWithYear = partialWithPlaceholder(formatDate, 2024, _, _);
console.log(formatWithYear(12, 25)); // 2024-12-25
const formatDecember = partialWithPlaceholder(formatDate, _, 12, _);
console.log(formatDecember(2024, 25)); // 2024-12-25
// Practical example: Array methods with partial
const numbers2 = [1, 2, 3, 4, 5];
// Create reusable predicates
const isGreaterThan = (threshold, value) => value > threshold;
const multiplyBy = (factor, value) => value * factor;
const isGreaterThan3 = partial2(isGreaterThan, 3);
const multiplyBy10 = partial2(multiplyBy, 10);
console.log(numbers2.filter(isGreaterThan3)); // [4, 5]
console.log(numbers2.map(multiplyBy10)); // [10, 20, 30, 40, 50]
// Partial for event handlers
function handleEvent(eventType, selector, handler, event) {
if (event.target.matches(selector)) {
handler(event);
}
}
const handleClick2 = partial2(handleEvent, 'click');
const handleButtonClick = partial2(handleClick2, 'button');
// Usage
// document.addEventListener('click', handleButtonClick((e) => {
// console.log('Button clicked');
// }));
// Combining curry and partial
const log = (level, module, message) => {
console.log(`[${level}] ${module}: ${message}`);
};
const curriedLog = curry(log);
// Create specialized loggers
const errorLog = curriedLog('ERROR');
const infoLog = curriedLog('INFO');
const appError = errorLog('App');
const dbInfo = infoLog('Database');
appError('Connection failed');
dbInfo('Query executed');
// Memoization with partial
function memoize2(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn(...args);
cache.set(key, result);
return result;
};
}
const expensiveCalc = (a, b, c) => {
console.log('Computing...');
return a + b + c;
};
const memoizedCalc2 = memoize2(expensiveCalc);
// Partially applied memoized function
const addTo10 = partial2(memoizedCalc2, 10);
console.log(addTo10(5, 5)); // Computing... 20
console.log(addTo10(5, 5)); // 20 (cached)
// Function application helper
const applyArgs = (...args) => fn => fn(...args);
const args = [1, 2, 3];
const result4 = applyArgs(...args)(add3);
console.log(result4); // 6
// Flip function (reverse argument order)
function flip(fn) {
return function(a, b) {
return fn(b, a);
};
}
const subtract2 = (a, b) => a - b;
const flippedSubtract = flip(subtract2);
console.log(subtract2(10, 3)); // 7
console.log(flippedSubtract(10, 3)); // -7
Key Points: Currying transforms multi-argument function to
sequence of unary functions. Partial application fixes some arguments, returns
function expecting remaining arguments. Currying enables point-free style and composition. Use
bind
or custom wrapper for partial application. Placeholder support allows flexible argument positioning. Both
techniques create specialized, reusable functions.
4. Immutability and Functional Data Structures
Immutability Principles
| Principle | Description | Benefit |
|---|---|---|
| No Mutation | Never modify existing data | Predictable state changes |
| Copy on Write | Create new copy when updating | Preserve history |
| Structural Sharing | Share unchanged parts | Memory efficiency |
| Persistent Data | All versions accessible | Time travel, undo/redo |
Immutable Operations
| Operation | Mutable (Avoid) | Immutable (Use) |
|---|---|---|
| Array Add | arr.push(item) | [...arr, item] or arr.concat(item) |
| Array Remove | arr.splice(idx, 1) | arr.filter((_, i) => i !== idx) |
| Array Update | arr[i] = value | arr.map((v, idx) => idx === i ? value : v) |
| Object Update | obj.prop = value | {...obj, prop: value} |
| Object Delete | delete obj.prop | const {prop, ...rest} = obj; return rest; |
| Object Merge | Object.assign(obj, updates) | {...obj, ...updates} |
Immutability Benefits
| Benefit | Description | Use Case |
|---|---|---|
| Predictability | Data doesn't change unexpectedly | Debugging, testing |
| Thread Safety | Safe concurrent access | Parallel processing |
| Change Detection | Reference equality checks | React shouldComponentUpdate |
| Time Travel | Keep all previous states | Undo/redo, debugging |
Example: Immutable operations
// MUTABLE (Avoid)
const arr = [1, 2, 3];
arr.push(4); // Modifies original
arr[0] = 10; // Modifies original
arr.sort(); // Modifies original
const obj = {name: 'John', age: 30};
obj.age = 31; // Modifies original
delete obj.name; // Modifies original
// IMMUTABLE (Prefer)
const arr2 = [1, 2, 3];
const arr3 = [...arr2, 4]; // New array
const arr4 = arr2.map((v, i) => i === 0 ? 10 : v); // New array
const arr5 = [...arr2].sort(); // New array
const obj2 = {name: 'John', age: 30};
const obj3 = {...obj2, age: 31}; // New object
const {name, ...obj4} = obj2; // New object without 'name'
// Array immutable operations
const numbers3 = [1, 2, 3, 4, 5];
// Add item
const withSix = [...numbers3, 6];
const withZero = [0, ...numbers3];
const withMiddle = [...numbers3.slice(0, 2), 99, ...numbers3.slice(2)];
// Remove item
const withoutThree = numbers3.filter(n => n !== 3);
const withoutIndex2 = numbers3.filter((_, i) => i !== 2);
// Update item
const doubled = numbers3.map(n => n * 2);
const updateIndex1 = numbers3.map((n, i) => i === 1 ? 99 : n);
// Object immutable operations
const user = {
name: 'John',
age: 30,
address: {
city: 'NYC',
country: 'USA'
}
};
// Update property
const olderUser = {...user, age: 31};
// Add property
const userWithEmail = {...user, email: 'john@example.com'};
// Remove property
const {age, ...userWithoutAge} = user;
// Deep update (nested)
const movedUser = {
...user,
address: {
...user.address,
city: 'LA'
}
};
// Nested updates helper
function updateNested(obj, path, value) {
const [first, ...rest] = path;
if (rest.length === 0) {
return {...obj, [first]: value};
}
return {
...obj,
[first]: updateNested(obj[first], rest, value)
};
}
const updated = updateNested(user, ['address', 'city'], 'LA');
console.log(updated);
// {name: 'John', age: 30, address: {city: 'LA', country: 'USA'}}
// Immutable array operations helper
const immutableArray = {
add: (arr, item) => [...arr, item],
addAt: (arr, index, item) => [
...arr.slice(0, index),
item,
...arr.slice(index)
],
remove: (arr, index) => [
...arr.slice(0, index),
...arr.slice(index + 1)
],
update: (arr, index, value) =>
arr.map((v, i) => i === index ? value : v),
updateWhere: (arr, predicate, updater) =>
arr.map(item => predicate(item) ? updater(item) : item)
};
const items = [{id: 1, name: 'A'}, {id: 2, name: 'B'}];
const newItems = immutableArray.updateWhere(
items,
item => item.id === 1,
item => ({...item, name: 'Updated A'})
);
console.log(newItems);
// Object.freeze for immutability
const frozen = Object.freeze({name: 'John', age: 30});
// frozen.age = 31; // Silent fail in non-strict, error in strict
// Deep freeze
function deepFreeze(obj) {
Object.freeze(obj);
Object.getOwnPropertyNames(obj).forEach(prop => {
if (obj[prop] !== null &&
(typeof obj[prop] === 'object' || typeof obj[prop] === 'function') &&
!Object.isFrozen(obj[prop])) {
deepFreeze(obj[prop]);
}
});
return obj;
}
const deepFrozen = deepFreeze({
name: 'John',
address: {city: 'NYC'}
});
// deepFrozen.address.city = 'LA'; // Error
// Immutable state management
class ImmutableState {
constructor(initialState) {
this.state = initialState;
this.history = [initialState];
this.currentIndex = 0;
}
setState(updates) {
const newState = {...this.state, ...updates};
// Remove any "future" states if we went back
this.history = this.history.slice(0, this.currentIndex + 1);
this.history.push(newState);
this.currentIndex++;
this.state = newState;
return newState;
}
getState() {
return this.state;
}
undo() {
if (this.currentIndex > 0) {
this.currentIndex--;
this.state = this.history[this.currentIndex];
}
return this.state;
}
redo() {
if (this.currentIndex < this.history.length - 1) {
this.currentIndex++;
this.state = this.history[this.currentIndex];
}
return this.state;
}
canUndo() {
return this.currentIndex > 0;
}
canRedo() {
return this.currentIndex < this.history.length - 1;
}
}
// Usage
const state = new ImmutableState({count: 0, user: null});
state.setState({count: 1});
state.setState({count: 2});
state.setState({count: 3});
console.log(state.getState()); // {count: 3, user: null}
console.log(state.undo()); // {count: 2, user: null}
console.log(state.undo()); // {count: 1, user: null}
console.log(state.redo()); // {count: 2, user: null}
// Lens pattern for immutable updates
function lens(getter, setter) {
return {
get: getter,
set: setter,
over: (fn, obj) => setter(fn(getter(obj)), obj)
};
}
const nameLens = lens(
user => user.name,
(name, user) => ({...user, name})
);
const cityLens = lens(
user => user.address.city,
(city, user) => ({
...user,
address: {...user.address, city}
})
);
const user2 = {name: 'John', address: {city: 'NYC'}};
const user3 = nameLens.set('Jane', user2);
const user4 = cityLens.over(city => city.toUpperCase(), user2);
console.log(user3); // {name: 'Jane', address: {city: 'NYC'}}
console.log(user4); // {name: 'John', address: {city: 'NYC'}}
Example: Functional data structures
// Immutable List
class List {
constructor(head, tail = null) {
this.head = head;
this.tail = tail;
}
static empty() {
return null;
}
static of(...values) {
return values.reduceRight((tail, head) =>
new List(head, tail), null
);
}
prepend(value) {
return new List(value, this);
}
map(fn) {
if (this.tail === null) {
return new List(fn(this.head));
}
return new List(fn(this.head), this.tail.map(fn));
}
filter(predicate) {
if (!predicate(this.head)) {
return this.tail ? this.tail.filter(predicate) : null;
}
return new List(
this.head,
this.tail ? this.tail.filter(predicate) : null
);
}
toArray() {
const result = [];
let current = this;
while (current !== null) {
result.push(current.head);
current = current.tail;
}
return result;
}
}
// Usage
const list = List.of(1, 2, 3, 4, 5);
const doubled2 = list.map(x => x * 2);
const evens2 = list.filter(x => x % 2 === 0);
console.log(doubled2.toArray()); // [2, 4, 6, 8, 10]
console.log(evens2.toArray()); // [2, 4]
// Immutable Stack
class Stack {
constructor(items = []) {
this.items = Object.freeze([...items]);
}
push(item) {
return new Stack([...this.items, item]);
}
pop() {
if (this.items.length === 0) {
return [null, this];
}
return [
this.items[this.items.length - 1],
new Stack(this.items.slice(0, -1))
];
}
peek() {
return this.items[this.items.length - 1] || null;
}
isEmpty() {
return this.items.length === 0;
}
size() {
return this.items.length;
}
}
// Usage
const stack1 = new Stack();
const stack2 = stack1.push(1);
const stack3 = stack2.push(2).push(3);
const [value, stack4] = stack3.pop();
console.log(value); // 3
console.log(stack4.peek()); // 2
console.log(stack3.peek()); // 3 (original unchanged)
// Immutable Queue
class Queue {
constructor(front = [], back = []) {
this.front = Object.freeze([...front]);
this.back = Object.freeze([...back]);
}
enqueue(item) {
return new Queue(this.front, [...this.back, item]);
}
dequeue() {
if (this.front.length === 0 && this.back.length === 0) {
return [null, this];
}
if (this.front.length === 0) {
const [first, ...rest] = this.back.reverse();
return [first, new Queue(rest, [])];
}
const [first, ...rest] = this.front;
return [first, new Queue(rest, this.back)];
}
peek() {
if (this.front.length > 0) {
return this.front[0];
}
return this.back[this.back.length - 1] || null;
}
isEmpty() {
return this.front.length === 0 && this.back.length === 0;
}
}
// Usage
const queue1 = new Queue();
const queue2 = queue1.enqueue(1).enqueue(2).enqueue(3);
const [val1, queue3] = queue2.dequeue();
const [val2, queue4] = queue3.dequeue();
console.log(val1); // 1
console.log(val2); // 2
// Immutable Tree
class TreeNode {
constructor(value, left = null, right = null) {
this.value = value;
this.left = left;
this.right = right;
}
insert(value) {
if (value < this.value) {
return new TreeNode(
this.value,
this.left ? this.left.insert(value) : new TreeNode(value),
this.right
);
} else {
return new TreeNode(
this.value,
this.left,
this.right ? this.right.insert(value) : new TreeNode(value)
);
}
}
contains(value) {
if (value === this.value) return true;
if (value < this.value) return this.left ? this.left.contains(value) : false;
return this.right ? this.right.contains(value) : false;
}
map(fn) {
return new TreeNode(
fn(this.value),
this.left ? this.left.map(fn) : null,
this.right ? this.right.map(fn) : null
);
}
}
// Usage
const tree1 = new TreeNode(5);
const tree2 = tree1.insert(3).insert(7).insert(1).insert(9);
console.log(tree2.contains(7)); // true
console.log(tree2.contains(10)); // false
console.log(tree1.contains(7)); // false (original unchanged)
// Record type with immutable updates
class Record {
constructor(data) {
Object.entries(data).forEach(([key, value]) => {
Object.defineProperty(this, key, {
value,
writable: false,
enumerable: true
});
});
Object.freeze(this);
}
set(key, value) {
return new Record({...this, [key]: value});
}
merge(updates) {
return new Record({...this, ...updates});
}
}
// Usage
const person = new Record({name: 'John', age: 30});
const older = person.set('age', 31);
const updated2 = person.merge({age: 31, city: 'NYC'});
console.log(person); // Record {name: 'John', age: 30}
console.log(older); // Record {name: 'John', age: 31}
console.log(updated2); // Record {name: 'John', age: 31, city: 'NYC'}
Key Points: Immutability means never modifying existing data.
Use spread operator, array methods (map, filter, concat) for immutable updates.
Object.freeze()
prevents mutations. Deep freeze for nested objects. Immutable data enables time travel (undo/redo), change
detection, predictable state. Functional data structures (List, Stack, Queue, Tree) use structural sharing.
5. Monads and Functional Error Handling
Monad Characteristics
| Characteristic | Description | Law |
|---|---|---|
| Container | Wraps a value | Type constructor |
| of/return | Put value in container | Left identity |
| map/fmap | Transform value inside | Functor law |
| flatMap/bind | Chain operations | Right identity, associativity |
Common Monads
| Monad | Purpose | Use Case |
|---|---|---|
| Maybe/Option | Handle null/undefined | Nullable values |
| Either/Result | Handle success/failure | Error handling |
| Promise | Handle async operations | Async workflows |
| Array | Handle multiple values | Non-determinism |
| IO | Handle side effects | Lazy evaluation |
Functional Error Handling Patterns
| Pattern | Approach | Benefit |
|---|---|---|
| Maybe | null/undefined as type | Explicit optionality |
| Either | Left (error) or Right (success) | Error as value |
| Result | Ok or Err variant | Type-safe errors |
| Validation | Accumulate errors | Multiple validations |
Example: Maybe monad
// Maybe Monad - handles null/undefined
class Maybe {
constructor(value) {
this.value = value;
}
static of(value) {
return new Maybe(value);
}
static nothing() {
return new Maybe(null);
}
isNothing() {
return this.value === null || this.value === undefined;
}
map(fn) {
return this.isNothing() ? Maybe.nothing() : Maybe.of(fn(this.value));
}
flatMap(fn) {
return this.isNothing() ? Maybe.nothing() : fn(this.value);
}
getOrElse(defaultValue) {
return this.isNothing() ? defaultValue : this.value;
}
filter(predicate) {
return this.isNothing() || !predicate(this.value)
? Maybe.nothing()
: this;
}
}
// Usage
const safeDivide = (a, b) =>
b === 0 ? Maybe.nothing() : Maybe.of(a / b);
const result5 = safeDivide(10, 2)
.map(x => x * 2)
.map(x => x + 5)
.getOrElse(0);
console.log(result5); // 15
const result6 = safeDivide(10, 0)
.map(x => x * 2) // Not executed
.map(x => x + 5) // Not executed
.getOrElse(0);
console.log(result6); // 0
// Safe property access
const getNestedProp = (obj, path) => {
return path.reduce((maybe, prop) =>
maybe.flatMap(o => Maybe.of(o[prop])),
Maybe.of(obj)
);
};
const user5 = {
name: 'John',
address: {
city: 'NYC'
}
};
const city = getNestedProp(user5, ['address', 'city']).getOrElse('Unknown');
const zip = getNestedProp(user5, ['address', 'zip']).getOrElse('Unknown');
console.log(city); // NYC
console.log(zip); // Unknown
// Maybe with find
const findUser = (id) => {
const users3 = [{id: 1, name: 'John'}, {id: 2, name: 'Jane'}];
const user = users3.find(u => u.id === id);
return user ? Maybe.of(user) : Maybe.nothing();
};
const userName = findUser(1)
.map(u => u.name)
.map(name => name.toUpperCase())
.getOrElse('Not Found');
console.log(userName); // JOHN
const missing = findUser(999)
.map(u => u.name)
.getOrElse('Not Found');
console.log(missing); // Not Found
Example: Either monad and Result pattern
// Either Monad - Left (error) or Right (success)
class Either {
constructor(value, isLeft = false) {
this.value = value;
this.isLeft = isLeft;
}
static left(value) {
return new Either(value, true);
}
static right(value) {
return new Either(value, false);
}
map(fn) {
return this.isLeft ? this : Either.right(fn(this.value));
}
flatMap(fn) {
return this.isLeft ? this : fn(this.value);
}
mapLeft(fn) {
return this.isLeft ? Either.left(fn(this.value)) : this;
}
fold(leftFn, rightFn) {
return this.isLeft ? leftFn(this.value) : rightFn(this.value);
}
getOrElse(defaultValue) {
return this.isLeft ? defaultValue : this.value;
}
}
// Usage with validation
const validateAge = (age) => {
if (typeof age !== 'number') {
return Either.left('Age must be a number');
}
if (age < 0) {
return Either.left('Age must be positive');
}
if (age < 18) {
return Either.left('Must be 18 or older');
}
return Either.right(age);
};
const result7 = validateAge(25)
.map(age => age * 2)
.fold(
error => `Error: ${error}`,
value => `Success: ${value}`
);
console.log(result7); // Success: 50
const result8 = validateAge(15)
.map(age => age * 2) // Not executed
.fold(
error => `Error: ${error}`,
value => `Success: ${value}`
);
console.log(result8); // Error: Must be 18 or older
// Result pattern (Rust-inspired)
class Result {
static ok(value) {
return {
isOk: true,
isErr: false,
value,
error: null
};
}
static err(error) {
return {
isOk: false,
isErr: true,
value: null,
error
};
}
}
// Usage
function parseJSON(str) {
try {
return Result.ok(JSON.parse(str));
} catch (error) {
return Result.err(error.message);
}
}
const validJSON = parseJSON('{"name": "John"}');
console.log(validJSON); // {isOk: true, value: {name: 'John'}, ...}
const invalidJSON = parseJSON('invalid json');
console.log(invalidJSON); // {isErr: true, error: '...', ...}
// Chain Result operations
function divideResult(a, b) {
return b === 0
? Result.err('Division by zero')
: Result.ok(a / b);
}
function sqrtResult(n) {
return n < 0
? Result.err('Cannot sqrt negative number')
: Result.ok(Math.sqrt(n));
}
function calculate(a, b) {
const divResult = divideResult(a, b);
if (divResult.isErr) {
return divResult;
}
return sqrtResult(divResult.value);
}
console.log(calculate(16, 4)); // {isOk: true, value: 2}
console.log(calculate(16, 0)); // {isErr: true, error: 'Division by zero'}
console.log(calculate(-16, 4)); // {isErr: true, error: 'Cannot sqrt negative...'}
// Try-catch wrapper
function tryCatch(fn) {
return (...args) => {
try {
return Either.right(fn(...args));
} catch (error) {
return Either.left(error);
}
};
}
const safeJSONParse = tryCatch(JSON.parse);
const result9 = safeJSONParse('{"valid": true}');
console.log(result9.fold(
err => `Parse error: ${err.message}`,
data => `Parsed: ${JSON.stringify(data)}`
));
// Validation with error accumulation
class Validation {
constructor(value, errors = []) {
this.value = value;
this.errors = errors;
}
static success(value) {
return new Validation(value, []);
}
static failure(error) {
return new Validation(null, [error]);
}
isValid() {
return this.errors.length === 0;
}
map(fn) {
return this.isValid()
? Validation.success(fn(this.value))
: this;
}
chain(fn) {
if (!this.isValid()) {
return this;
}
const result = fn(this.value);
return new Validation(result.value, [...this.errors, ...result.errors]);
}
}
// Validate user input
function validateUser(data) {
const errors = [];
if (!data.email || !data.email.includes('@')) {
errors.push('Invalid email');
}
if (!data.password || data.password.length < 8) {
errors.push('Password must be at least 8 characters');
}
if (!data.age || data.age < 18) {
errors.push('Must be 18 or older');
}
return errors.length === 0
? Validation.success(data)
: new Validation(null, errors);
}
const validation1 = validateUser({
email: 'john@example.com',
password: 'secret123',
age: 25
});
console.log(validation1); // {value: {...}, errors: []}
const validation2 = validateUser({
email: 'invalid',
password: 'short',
age: 15
});
console.log(validation2);
// {value: null, errors: ['Invalid email', 'Password...', 'Must be 18...']}
Key Points: Monads are containers with map and flatMap methods.
Maybe monad handles null/undefined safely. Either
monad represents success (Right) or failure (Left). Result pattern from Rust (Ok/Err). Validation monad
accumulates errors. Functional error handling treats errors as values, not exceptions. Railway-oriented
programming with Either/Result enables composable error handling.
6. Function Pipelines and Data Transformation
Pipeline Patterns
| Pattern | Flow | Usage |
|---|---|---|
| Pipe | Left-to-right | data |> fn1 |> fn2 |> fn3 |
| Compose | Right-to-left | fn3(fn2(fn1(data))) |
| Method Chain | Sequential methods | obj.method1().method2() |
| Transducer | Composed transformations | Single pass over data |
Data Transformation Operations
| Operation | Purpose | Example |
|---|---|---|
| Map | Transform each element | arr.map(x => x * 2) |
| Filter | Select elements | arr.filter(x => x > 5) |
| Reduce | Aggregate to single value | arr.reduce((sum, x) => sum + x) |
| FlatMap | Map then flatten | arr.flatMap(x => [x, x*2]) |
| GroupBy | Group by property | Custom reduce |
| Partition | Split by predicate | [pass, fail] arrays |
Pipeline Benefits
| Benefit | Description | Impact |
|---|---|---|
| Readability | Sequential, declarative | Easier to understand |
| Composability | Build from small functions | Reusable components |
| Testability | Test each step independently | Better test coverage |
| Maintainability | Easy to add/remove steps | Flexible architecture |
Example: Pipeline operators and data transformation
// Pipe function (left-to-right)
const pipe2 = (...fns) => (value) =>
fns.reduce((acc, fn) => fn(acc), value);
// Example: Process user data
const users4 = [
{name: 'john doe', age: 17, active: true, score: 85},
{name: 'jane smith', age: 25, active: false, score: 92},
{name: 'bob jones', age: 30, active: true, score: 78},
{name: 'alice brown', age: 22, active: true, score: 95}
];
// Small, focused functions
const filterActive2 = users => users.filter(u => u.active);
const filterAdults2 = users => users.filter(u => u.age >= 18);
const sortByScore = users => [...users].sort((a, b) => b.score - a.score);
const takeFive = users => users.slice(0, 5);
const capitalizeNames2 = users => users.map(u => ({
...u,
name: u.name.split(' ').map(w =>
w.charAt(0).toUpperCase() + w.slice(1)
).join(' ')
}));
const extractNames2 = users => users.map(u => u.name);
// Build pipeline
const processUsers2 = pipe2(
filterActive2,
filterAdults2,
sortByScore,
takeFive,
capitalizeNames2,
extractNames2
);
console.log(processUsers2(users4));
// ['Alice Brown', 'Bob Jones']
// Alternative: Point-free style
const getTopActiveAdultNames = pipe2(
filterActive2,
filterAdults2,
sortByScore,
takeFive,
capitalizeNames2,
extractNames2
);
// Data transformation pipeline
const numbers4 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const transform = pipe2(
arr => arr.filter(x => x % 2 === 0),
arr => arr.map(x => x * x),
arr => arr.reduce((sum, x) => sum + x, 0),
sum => Math.sqrt(sum)
);
console.log(transform(numbers4)); // Square root of sum of squares of evens
// Async pipeline
const asyncPipe2 = (...fns) => async (value) => {
let result = value;
for (const fn of fns) {
result = await fn(result);
}
return result;
};
const fetchUserData = async (id) => {
// Simulated fetch
return {id, name: 'John', posts: []};
};
const enrichWithPosts = async (user) => {
// Simulated fetch
const posts = [{id: 1, title: 'Post 1'}];
return {...user, posts};
};
const formatUserData = (user) => ({
username: user.name.toUpperCase(),
postCount: user.posts.length
});
const getUserInfo = asyncPipe2(
fetchUserData,
enrichWithPosts,
formatUserData
);
// await getUserInfo(123);
// Tap function for debugging
const tap = (fn) => (value) => {
fn(value);
return value;
};
const debugPipeline = pipe2(
filterActive2,
tap(data => console.log('After filter:', data.length)),
filterAdults2,
tap(data => console.log('After adults filter:', data.length)),
sortByScore,
tap(data => console.log('After sort:', data[0]))
);
// GroupBy implementation
const groupBy = (key) => (array) => {
return array.reduce((groups, item) => {
const group = item[key];
if (!groups[group]) {
groups[group] = [];
}
groups[group].push(item);
return groups;
}, {});
};
const byAge = groupBy('age');
const grouped = byAge(users4);
console.log(grouped);
// Partition function
const partition = (predicate) => (array) => {
return array.reduce(
([pass, fail], item) =>
predicate(item)
? [[...pass, item], fail]
: [pass, [...fail, item]],
[[], []]
);
};
const [adults, minors] = partition(u => u.age >= 18)(users4);
console.log('Adults:', adults.length);
console.log('Minors:', minors.length);
// Map with index
const mapWithIndex = (fn) => (array) =>
array.map((item, index) => fn(item, index));
const addIndex = mapWithIndex((item, index) => ({
...item,
index
}));
// Chunk array
const chunk = (size) => (array) => {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
};
const chunked = chunk(3)([1, 2, 3, 4, 5, 6, 7, 8]);
console.log(chunked); // [[1,2,3], [4,5,6], [7,8]]
// Flatten array
const flatten = (array) =>
array.reduce((acc, val) =>
Array.isArray(val) ? acc.concat(flatten(val)) : acc.concat(val),
[]);
const nested = [1, [2, [3, 4], 5], 6];
console.log(flatten(nested)); // [1, 2, 3, 4, 5, 6]
// Unique values
const unique = (array) => [...new Set(array)];
const duplicates = [1, 2, 2, 3, 3, 3, 4];
console.log(unique(duplicates)); // [1, 2, 3, 4]
// Pluck property
const pluck = (prop) => (array) =>
array.map(item => item[prop]);
const names = pluck('name')(users4);
console.log(names);
// Complex transformation pipeline
const processOrders = pipe2(
// Filter valid orders
orders => orders.filter(o => o.items.length > 0),
// Add total
orders => orders.map(o => ({
...o,
total: o.items.reduce((sum, item) =>
sum + (item.price * item.quantity), 0
)
})),
// Filter high value
orders => orders.filter(o => o.total > 100),
// Sort by total
orders => [...orders].sort((a, b) => b.total - a.total),
// Group by customer
groupBy('customerId')
);
const orders = [
{customerId: 1, items: [{price: 10, quantity: 2}]},
{customerId: 1, items: [{price: 50, quantity: 3}]},
{customerId: 2, items: [{price: 20, quantity: 1}]},
{customerId: 2, items: [{price: 100, quantity: 2}]}
];
console.log(processOrders(orders));
// Memoized pipeline
const memoizePipeline = (pipeline) => {
const cache = new Map();
return (input) => {
const key = JSON.stringify(input);
if (cache.has(key)) {
return cache.get(key);
}
const result = pipeline(input);
cache.set(key, result);
return result;
};
};
const expensivePipeline = memoizePipeline(
pipe2(
arr => arr.map(x => x * x),
arr => arr.filter(x => x > 10),
arr => arr.reduce((sum, x) => sum + x, 0)
)
);
console.log(expensivePipeline([1, 2, 3, 4, 5])); // Computed
console.log(expensivePipeline([1, 2, 3, 4, 5])); // Cached
Key Points: Function pipelines compose small functions into
complex transformations. Pipe (left-to-right) vs compose (right-to-left). Common operations: map, filter,
reduce, flatMap, groupBy, partition. Benefits: readability, testability, maintainability. Use tap for
debugging. Async pipelines for async operations. Memoize pipelines for performance. Build complex data
transformations from simple, pure functions.
Section 23 Summary: Functional Programming Concepts
- Pure Functions: Deterministic, no side effects, same input → same output, easier to test and reason about
- Side Effects: Mutation, I/O, DOM, network, random - isolate at program boundaries, use pure core/impure shell
- Higher-Order Functions: Accept functions as arguments or return functions (map, filter, reduce, decorators)
- Function Composition: Combine functions with pipe (left-to-right) or compose (right-to-left)
- Currying: Transform f(a,b,c) to f(a)(b)(c), one argument at a time, enables partial application
- Partial Application: Fix some arguments, return function expecting remaining args, use bind or custom wrapper
- Immutability: Never modify data, use spread/map/filter for updates, Object.freeze, functional data structures
- Data Structures: Immutable List, Stack, Queue, Tree - structural sharing for efficiency
- Monads: Containers with map/flatMap - Maybe (null handling), Either (error handling), Result pattern
- Error Handling: Errors as values not exceptions, Either for success/failure, Validation for error accumulation
- Pipelines: Compose transformations (map, filter, reduce, groupBy, partition) for data processing
- Benefits: Functional programming enables predictability, testability, composability, maintainability, fewer bugs