Functional Programming

Pure functions, immutability, higher-order functions, currying, and composition

Functional Programming in JavaScript

Functional programming (FP) is a paradigm that treats computation as the evaluation of mathematical functions. It emphasizes immutability, pure functions, and declarative code. JavaScript supports FP patterns alongside object-oriented programming.

Core Principles

  • Pure Functions — Same input always produces same output, no side effects
  • Immutability — Data is never modified, only transformed into new data
  • First-Class Functions — Functions are values that can be passed around
  • Declarative — Describe what to do, not how to do it

Pure Functions

// ❌ Impure - modifies external state
let total = 0;
function addToTotal(value) {
  total += value; // Side effect!
  return total;
}

// ❌ Impure - depends on external state
function getDiscount(price) {
  return price * currentDiscountRate; // External dependency
}

// ❌ Impure - non-deterministic
function getRandomGreeting(name) {
  const greetings = ["Hi", "Hello", "Hey"];
  const random = Math.floor(Math.random() * 3);
  return `${greetings[random]}, ${name}`; // Different outputs!
}

// ✅ Pure - same input, same output, no side effects
function add(a, b) {
  return a + b;
}

function getDiscount(price, discountRate) {
  return price * discountRate;
}

function formatUser(user) {
  return `${user.firstName} ${user.lastName}`;
}

Immutability

// ❌ Mutating data
const user = { name: "Alice", age: 30 };
user.age = 31; // Mutation!

const numbers = [1, 2, 3];
numbers.push(4); // Mutation!

// ✅ Creating new data
const updatedUser = { ...user, age: 31 };
const newNumbers = [...numbers, 4];

// Array methods that return new arrays
const doubled = numbers.map(n => n * 2);     // [2, 4, 6]
const evens = numbers.filter(n => n % 2 === 0); // [2]
const sum = numbers.reduce((a, b) => a + b, 0); // 6

// Deep immutability with nested objects
const state = {
  user: { name: "Alice", settings: { theme: "dark" } },
  posts: [1, 2, 3]
};

// Update nested property immutably
const newState = {
  ...state,
  user: {
    ...state.user,
    settings: {
      ...state.user.settings,
      theme: "light"
    }
  }
};

// Object.freeze for shallow immutability
const frozen = Object.freeze({ a: 1 });
frozen.a = 2; // Silently fails (throws in strict mode)

Higher-Order Functions

Functions that take functions as arguments or return functions:

// Array methods are higher-order functions
const users = [
  { name: "Alice", age: 30 },
  { name: "Bob", age: 25 },
  { name: "Charlie", age: 35 }
];

// map - transform each element
const names = users.map(user => user.name);
// ["Alice", "Bob", "Charlie"]

// filter - keep elements matching predicate
const adults = users.filter(user => user.age >= 30);
// [{ name: "Alice"... }, { name: "Charlie"... }]

// reduce - accumulate into single value
const totalAge = users.reduce((sum, user) => sum + user.age, 0);
// 90

// find - first matching element
const bob = users.find(user => user.name === "Bob");

// some / every - test predicates
const hasAdults = users.some(user => user.age >= 18);  // true
const allAdults = users.every(user => user.age >= 18); // true

// Chaining
const result = users
  .filter(u => u.age >= 30)
  .map(u => u.name)
  .sort();
// ["Alice", "Charlie"]

Function Composition

// Combine small functions into larger ones
const add10 = x => x + 10;
const multiply2 = x => x * 2;
const subtract5 = x => x - 5;

// Manual composition
const result = subtract5(multiply2(add10(5))); // ((5+10)*2)-5 = 25

// Compose function (right to left)
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);

const calculate = compose(subtract5, multiply2, add10);
calculate(5); // 25

// Pipe function (left to right) - more readable
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);

const process = pipe(
  add10,      // 5 + 10 = 15
  multiply2,  // 15 * 2 = 30
  subtract5   // 30 - 5 = 25
);

process(5); // 25

// Practical example
const processUsers = pipe(
  users => users.filter(u => u.active),
  users => users.map(u => u.email),
  emails => emails.map(e => e.toLowerCase()),
  emails => [...new Set(emails)] // unique
);

Currying

Transform a function with multiple arguments into a sequence of functions:

// Regular function
function add(a, b, c) {
  return a + b + c;
}
add(1, 2, 3); // 6

// Curried version
function curriedAdd(a) {
  return function(b) {
    return function(c) {
      return a + b + c;
    };
  };
}
curriedAdd(1)(2)(3); // 6

// Arrow function currying
const curriedAdd = a => b => c => a + b + c;

// Partial application
const add1 = curriedAdd(1);
const add1and2 = add1(2);
add1and2(3); // 6

// Practical use case
const multiply = a => b => a * b;
const double = multiply(2);
const triple = multiply(3);

[1, 2, 3].map(double); // [2, 4, 6]
[1, 2, 3].map(triple); // [3, 6, 9]

// Generic curry function
const curry = (fn) => {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    }
    return (...more) => curried(...args, ...more);
  };
};

const curriedSum = curry((a, b, c) => a + b + c);
curriedSum(1)(2)(3);   // 6
curriedSum(1, 2)(3);   // 6
curriedSum(1)(2, 3);   // 6

Useful FP Utilities

// Identity
const identity = x => x;

// Constant
const constant = x => () => x;
const always5 = constant(5);
always5(); // 5

// Tap (for debugging in pipelines)
const tap = fn => x => { fn(x); return x; };

pipe(
  add10,
  tap(console.log), // Log intermediate value
  multiply2
)(5);

// Memoization
const memoize = fn => {
  const cache = new Map();
  return (...args) => {
    const key = JSON.stringify(args);
    if (!cache.has(key)) {
      cache.set(key, fn(...args));
    }
    return cache.get(key);
  };
};

const expensiveFn = memoize((n) => {
  console.log("Computing...");
  return n * n;
});

expensiveFn(5); // "Computing..." → 25
expensiveFn(5); // 25 (from cache)

// Partial application
const partial = (fn, ...args) => (...more) => fn(...args, ...more);

const greet = (greeting, name) => `${greeting}, ${name}!`;
const sayHello = partial(greet, "Hello");
sayHello("Alice"); // "Hello, Alice!"

💡 Key Takeaways

  • • Pure functions are predictable, testable, and cacheable
  • • Prefer immutable updates with spread operator and array methods
  • • Use map, filter, reduce for data transformations
  • • Compose small functions into larger pipelines
  • • Currying enables partial application and reusable functions
  • • FP makes code more declarative and easier to reason about