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