JavaScript Functional Programming: Pure Functions, Immutability, and FP Patterns
Master functional programming in JavaScript. Learn pure functions, immutability, higher-order functions, closures, currying, and functional composition.
Functional programming (FP) is a programming paradigm that treats computation as the evaluation of mathematical functions. In JavaScript, FP principles can lead to more predictable, testable, and maintainable code. This guide covers core FP concepts and their practical application.
Core Principles of Functional Programming
Pure Functions
Pure functions are the foundation of functional programming. They always return the same output for the same input and have no side effects.
// Pure Functions - Predictable and testable
class PureFunctions {
// Pure function - same input always produces same output
static add(a, b) {
return a + b;
}
// Pure function - no side effects
static multiply(x, y) {
return x * y;
}
// Pure function - doesn't modify input
static addToArray(arr, item) {
return [...arr, item]; // Returns new array
}
// Pure function - string manipulation
static capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}
// Pure function - object transformation
static updateUser(user, updates) {
return { ...user, ...updates }; // Returns new object
}
// Pure function - array filtering
static filterAdults(people) {
return people.filter((person) => person.age >= 18);
}
// Pure function - mathematical calculation
static calculateDistance(point1, point2) {
const dx = point2.x - point1.x;
const dy = point2.y - point1.y;
return Math.sqrt(dx * dx + dy * dy);
}
// Pure function - data validation
static isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
}
// Impure Functions - Avoid these patterns
class ImpureFunctions {
constructor() {
this.counter = 0;
this.data = [];
}
// Impure - modifies external state
incrementCounter() {
this.counter++; // Side effect
return this.counter;
}
// Impure - depends on external state
getCounterValue() {
return this.counter; // Depends on mutable state
}
// Impure - modifies input parameter
addToDataMutating(item) {
this.data.push(item); // Mutates external state
return this.data;
}
// Impure - has side effects
logAndAdd(a, b) {
console.log(`Adding ${a} and ${b}`); // Side effect
return a + b;
}
// Impure - depends on current time
greetBasedOnTime() {
const hour = new Date().getHours(); // External dependency
return hour < 12 ? 'Good morning' : 'Good afternoon';
}
}
// Pure Function Utilities
class FunctionalUtils {
// Compose multiple pure functions
static compose(...functions) {
return (value) => {
return functions.reduceRight((acc, fn) => fn(acc), value);
};
}
// Pipe functions left to right
static pipe(...functions) {
return (value) => {
return functions.reduce((acc, fn) => fn(acc), value);
};
}
// Memoization for expensive pure functions
static 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.apply(this, args);
cache.set(key, result);
return result;
};
}
// Partial application
static partial(fn, ...partialArgs) {
return function (...remainingArgs) {
return fn(...partialArgs, ...remainingArgs);
};
}
}
// Usage examples
const { add, multiply, updateUser } = PureFunctions;
console.log(add(2, 3)); // Always returns 5
console.log(multiply(4, 5)); // Always returns 20
const user = { name: 'John', age: 25 };
const updatedUser = updateUser(user, { age: 26 });
console.log(user); // Original unchanged
console.log(updatedUser); // New object with updates
// Function composition
const addOne = (x) => x + 1;
const double = (x) => x * 2;
const square = (x) => x * x;
const addOneThenDouble = FunctionalUtils.compose(double, addOne);
const pipeline = FunctionalUtils.pipe(addOne, double, square);
console.log(addOneThenDouble(3)); // (3 + 1) * 2 = 8
console.log(pipeline(3)); // ((3 + 1) * 2)² = 64
// Memoized expensive function
const expensiveCalculation = FunctionalUtils.memoize((n) => {
console.log(`Computing for ${n}...`);
return n * n * n;
});
console.log(expensiveCalculation(5)); // Computes and caches
console.log(expensiveCalculation(5)); // Returns cached result
Immutability
Immutability means that once data is created, it cannot be changed. Instead of modifying existing data, you create new data structures.
// Immutable Data Structures
class ImmutableArray {
constructor(data = []) {
this._data = Object.freeze([...data]);
}
// Returns new instance with added item
push(item) {
return new ImmutableArray([...this._data, item]);
}
// Returns new instance with item removed
pop() {
return new ImmutableArray(this._data.slice(0, -1));
}
// Returns new instance with item at index
set(index, value) {
const newData = [...this._data];
newData[index] = value;
return new ImmutableArray(newData);
}
// Returns new instance with filtered items
filter(predicate) {
return new ImmutableArray(this._data.filter(predicate));
}
// Returns new instance with mapped items
map(mapper) {
return new ImmutableArray(this._data.map(mapper));
}
// Returns new instance with reduced result
reduce(reducer, initialValue) {
return this._data.reduce(reducer, initialValue);
}
// Get item at index (read-only)
get(index) {
return this._data[index];
}
// Get length
get length() {
return this._data.length;
}
// Convert to regular array
toArray() {
return [...this._data];
}
// Iterator support
[Symbol.iterator]() {
return this._data[Symbol.iterator]();
}
}
class ImmutableObject {
constructor(data = {}) {
this._data = Object.freeze({ ...data });
}
// Returns new instance with property set
set(key, value) {
return new ImmutableObject({
...this._data,
[key]: value,
});
}
// Returns new instance with nested property set
setIn(path, value) {
const keys = path.split('.');
const newData = this._deepSet(this._data, keys, value);
return new ImmutableObject(newData);
}
_deepSet(obj, keys, value) {
if (keys.length === 1) {
return { ...obj, [keys[0]]: value };
}
const [first, ...rest] = keys;
return {
...obj,
[first]: this._deepSet(obj[first] || {}, rest, value),
};
}
// Returns new instance with property deleted
delete(key) {
const { [key]: deleted, ...rest } = this._data;
return new ImmutableObject(rest);
}
// Returns new instance with multiple properties merged
merge(other) {
return new ImmutableObject({
...this._data,
...other,
});
}
// Get property value
get(key) {
return this._data[key];
}
// Get nested property value
getIn(path) {
return path.split('.').reduce((obj, key) => obj && obj[key], this._data);
}
// Check if property exists
has(key) {
return key in this._data;
}
// Get all keys
keys() {
return Object.keys(this._data);
}
// Get all values
values() {
return Object.values(this._data);
}
// Convert to regular object
toObject() {
return { ...this._data };
}
}
// Immutability Helpers
class ImmutabilityHelpers {
// Deep freeze object and all nested objects
static deepFreeze(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
Object.getOwnPropertyNames(obj).forEach((name) => {
const value = obj[name];
if (value && typeof value === 'object') {
this.deepFreeze(value);
}
});
return Object.freeze(obj);
}
// Deep clone object
static deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (obj instanceof Date) {
return new Date(obj.getTime());
}
if (obj instanceof Array) {
return obj.map((item) => this.deepClone(item));
}
const cloned = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
cloned[key] = this.deepClone(obj[key]);
}
}
return cloned;
}
// Update nested object immutably
static updateIn(obj, path, updater) {
const keys = path.split('.');
const update = (current, keyPath) => {
if (keyPath.length === 1) {
const [key] = keyPath;
return {
...current,
[key]: updater(current[key]),
};
}
const [first, ...rest] = keyPath;
return {
...current,
[first]: update(current[first] || {}, rest),
};
};
return update(obj, keys);
}
// Merge objects deeply
static deepMerge(target, source) {
const result = { ...target };
for (const key in source) {
if (source.hasOwnProperty(key)) {
if (
typeof source[key] === 'object' &&
source[key] !== null &&
!Array.isArray(source[key]) &&
typeof result[key] === 'object' &&
result[key] !== null &&
!Array.isArray(result[key])
) {
result[key] = this.deepMerge(result[key], source[key]);
} else {
result[key] = source[key];
}
}
}
return result;
}
}
// Usage examples
const immutableArr = new ImmutableArray([1, 2, 3]);
const newArr = immutableArr.push(4).push(5);
console.log(immutableArr.toArray()); // [1, 2, 3] - unchanged
console.log(newArr.toArray()); // [1, 2, 3, 4, 5]
const immutableObj = new ImmutableObject({
name: 'John',
address: {
city: 'New York',
zip: '10001',
},
});
const updated = immutableObj
.set('age', 30)
.setIn('address.city', 'San Francisco');
console.log(immutableObj.toObject()); // Original unchanged
console.log(updated.toObject()); // Updated version
// Using immutability helpers
const frozenData = ImmutabilityHelpers.deepFreeze({
users: [{ name: 'Alice', settings: { theme: 'dark' } }],
});
const updatedData = ImmutabilityHelpers.updateIn(
frozenData,
'users.0.settings.theme',
() => 'light'
);
Higher-Order Functions
Higher-order functions either take functions as arguments, return functions, or both. They're essential for functional programming patterns.
// Higher-Order Function Patterns
class HigherOrderFunctions {
// Function that returns a function
static createMultiplier(factor) {
return function (number) {
return number * factor;
};
}
// Function that takes a function as argument
static applyTwice(fn, value) {
return fn(fn(value));
}
// Function factory for validators
static createValidator(validationFn, errorMessage) {
return function (value) {
const isValid = validationFn(value);
return {
isValid,
value,
error: isValid ? null : errorMessage,
};
};
}
// Conditional execution
static when(predicate, fn) {
return function (value) {
return predicate(value) ? fn(value) : value;
};
}
// Function that creates event handlers
static createEventHandler(callback, transform = (x) => x) {
return function (event) {
const transformedData = transform(event);
callback(transformedData);
};
}
// Retry mechanism
static withRetry(fn, maxAttempts = 3, delay = 1000) {
return async function (...args) {
let attempts = 0;
while (attempts < maxAttempts) {
try {
return await fn(...args);
} catch (error) {
attempts++;
if (attempts >= maxAttempts) {
throw error;
}
await new Promise((resolve) => setTimeout(resolve, delay));
}
}
};
}
// Caching/Memoization
static withCache(fn, cacheSize = 100) {
const cache = new Map();
const accessOrder = [];
return function (...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
// Move to end of access order
const index = accessOrder.indexOf(key);
accessOrder.splice(index, 1);
accessOrder.push(key);
return cache.get(key);
}
const result = fn(...args);
// Add to cache
cache.set(key, result);
accessOrder.push(key);
// Evict oldest if cache is full
if (cache.size > cacheSize) {
const oldest = accessOrder.shift();
cache.delete(oldest);
}
return result;
};
}
// Timing decorator
static withTiming(fn, label) {
return function (...args) {
const start = performance.now();
const result = fn(...args);
const end = performance.now();
console.log(`${label || fn.name} took ${end - start} milliseconds`);
return result;
};
}
// Logging decorator
static withLogging(fn, logLevel = 'info') {
return function (...args) {
console[logLevel](`Calling ${fn.name} with args:`, args);
const result = fn(...args);
console[logLevel](`${fn.name} returned:`, result);
return result;
};
}
}
// Array Higher-Order Functions
class ArrayFunctional {
// Custom map implementation
static map(array, mapper) {
const result = [];
for (let i = 0; i < array.length; i++) {
result.push(mapper(array[i], i, array));
}
return result;
}
// Custom filter implementation
static filter(array, predicate) {
const result = [];
for (let i = 0; i < array.length; i++) {
if (predicate(array[i], i, array)) {
result.push(array[i]);
}
}
return result;
}
// Custom reduce implementation
static reduce(array, reducer, initialValue) {
let accumulator = initialValue;
let startIndex = 0;
if (initialValue === undefined && array.length > 0) {
accumulator = array[0];
startIndex = 1;
}
for (let i = startIndex; i < array.length; i++) {
accumulator = reducer(accumulator, array[i], i, array);
}
return accumulator;
}
// Find first matching element
static find(array, predicate) {
for (let i = 0; i < array.length; i++) {
if (predicate(array[i], i, array)) {
return array[i];
}
}
return undefined;
}
// Check if all elements match predicate
static every(array, predicate) {
for (let i = 0; i < array.length; i++) {
if (!predicate(array[i], i, array)) {
return false;
}
}
return true;
}
// Check if any element matches predicate
static some(array, predicate) {
for (let i = 0; i < array.length; i++) {
if (predicate(array[i], i, array)) {
return true;
}
}
return false;
}
// Group array elements by key
static groupBy(array, keySelector) {
return array.reduce((groups, item) => {
const key = keySelector(item);
if (!groups[key]) {
groups[key] = [];
}
groups[key].push(item);
return groups;
}, {});
}
// Partition array into two groups
static partition(array, predicate) {
return array.reduce(
(acc, item, index) => {
const target = predicate(item, index, array) ? 0 : 1;
acc[target].push(item);
return acc;
},
[[], []]
);
}
// Flatten nested arrays
static flatten(array, depth = 1) {
return depth > 0
? array.reduce(
(acc, val) =>
acc.concat(Array.isArray(val) ? this.flatten(val, depth - 1) : val),
[]
)
: array.slice();
}
// Unique elements
static unique(array, keySelector = (x) => x) {
const seen = new Set();
return array.filter((item) => {
const key = keySelector(item);
if (seen.has(key)) {
return false;
}
seen.add(key);
return true;
});
}
}
// Usage examples
const double = HigherOrderFunctions.createMultiplier(2);
const triple = HigherOrderFunctions.createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(4)); // 12
const addOne = (x) => x + 1;
const result = HigherOrderFunctions.applyTwice(addOne, 5); // 7
// Validators
const isEmail = HigherOrderFunctions.createValidator(
(value) => /\S+@\S+\.\S+/.test(value),
'Invalid email format'
);
const isMinLength = (min) =>
HigherOrderFunctions.createValidator(
(value) => value.length >= min,
`Must be at least ${min} characters`
);
console.log(isEmail('test@example.com')); // { isValid: true, value: 'test@example.com', error: null }
console.log(isMinLength(8)('password')); // { isValid: true, value: 'password', error: null }
// Array operations
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const doubled = ArrayFunctional.map(numbers, (x) => x * 2);
const evens = ArrayFunctional.filter(numbers, (x) => x % 2 === 0);
const sum = ArrayFunctional.reduce(numbers, (acc, val) => acc + val, 0);
console.log(doubled); // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
console.log(evens); // [2, 4, 6, 8, 10]
console.log(sum); // 55
const people = [
{ name: 'Alice', age: 25, department: 'Engineering' },
{ name: 'Bob', age: 30, department: 'Marketing' },
{ name: 'Charlie', age: 35, department: 'Engineering' },
];
const byDepartment = ArrayFunctional.groupBy(people, (p) => p.department);
console.log(byDepartment);
// {
// Engineering: [{ name: 'Alice', ... }, { name: 'Charlie', ... }],
// Marketing: [{ name: 'Bob', ... }]
// }
Function Composition and Currying
Function composition and currying are powerful techniques for building complex functionality from simple functions.
// Function Composition Utilities
class Composition {
// Basic composition (right to left)
static compose(...functions) {
if (functions.length === 0) {
return (arg) => arg;
}
if (functions.length === 1) {
return functions[0];
}
return functions.reduce(
(a, b) =>
(...args) =>
a(b(...args))
);
}
// Pipe (left to right)
static pipe(...functions) {
if (functions.length === 0) {
return (arg) => arg;
}
if (functions.length === 1) {
return functions[0];
}
return functions.reduce(
(a, b) =>
(...args) =>
b(a(...args))
);
}
// Async composition
static composeAsync(...functions) {
return async function (value) {
let result = value;
for (let i = functions.length - 1; i >= 0; i--) {
result = await functions[i](result);
}
return result;
};
}
// Async pipe
static pipeAsync(...functions) {
return async function (value) {
let result = value;
for (const fn of functions) {
result = await fn(result);
}
return result;
};
}
// Conditional composition
static composeIf(predicate, ...functions) {
const composed = this.compose(...functions);
return function (value) {
return predicate(value) ? composed(value) : value;
};
}
// Composition with error handling
static composeSafe(...functions) {
return function (value) {
try {
return functions.reduceRight((acc, fn) => fn(acc), value);
} catch (error) {
return { error: error.message, value };
}
};
}
}
// Currying Utilities
class Currying {
// Basic currying
static curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function (...args2) {
return curried.apply(this, args.concat(args2));
};
}
};
}
// Partial application
static partial(fn, ...partialArgs) {
return function (...remainingArgs) {
return fn(...partialArgs, ...remainingArgs);
};
}
// Right partial application
static partialRight(fn, ...partialArgs) {
return function (...remainingArgs) {
return fn(...remainingArgs, ...partialArgs);
};
}
// Placeholder-based currying
static curryWithPlaceholder(fn, placeholder = Currying.PLACEHOLDER) {
return function curried(...args) {
const hasPlaceholder = args.some((arg) => arg === placeholder);
if (!hasPlaceholder && args.length >= fn.length) {
return fn.apply(this, args);
}
return function (...nextArgs) {
let nextIndex = 0;
const mergedArgs = args.map((arg) =>
arg === placeholder && nextIndex < nextArgs.length
? nextArgs[nextIndex++]
: arg
);
return curried(...mergedArgs, ...nextArgs.slice(nextIndex));
};
};
}
// Placeholder symbol
static PLACEHOLDER = Symbol('curry-placeholder');
}
// Functional Programming Utilities
class FunctionalProgramming {
// Identity function
static identity(x) {
return x;
}
// Constant function
static constant(value) {
return () => value;
}
// Flip function arguments
static flip(fn) {
return function (a, b, ...rest) {
return fn(b, a, ...rest);
};
}
// Once function - only executes once
static once(fn) {
let called = false;
let result;
return function (...args) {
if (!called) {
called = true;
result = fn.apply(this, args);
}
return result;
};
}
// Debounce function
static debounce(fn, delay) {
let timeoutId;
return function (...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), delay);
};
}
// Throttle function
static throttle(fn, limit) {
let inThrottle;
return function (...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
// Maybe monad for null safety
static Maybe = class {
constructor(value) {
this.value = value;
}
static of(value) {
return new FunctionalProgramming.Maybe(value);
}
static nothing() {
return new FunctionalProgramming.Maybe(null);
}
isNothing() {
return this.value === null || this.value === undefined;
}
map(fn) {
return this.isNothing()
? FunctionalProgramming.Maybe.nothing()
: FunctionalProgramming.Maybe.of(fn(this.value));
}
flatMap(fn) {
return this.isNothing()
? FunctionalProgramming.Maybe.nothing()
: fn(this.value);
}
filter(predicate) {
return this.isNothing() || !predicate(this.value)
? FunctionalProgramming.Maybe.nothing()
: this;
}
getOrElse(defaultValue) {
return this.isNothing() ? defaultValue : this.value;
}
};
}
// Practical Examples
class FunctionalExamples {
// Data processing pipeline
static createDataProcessor() {
const parseCSV = (csvString) => {
return csvString.split('\n').map((line) => line.split(','));
};
const removeHeader = (rows) => {
return rows.slice(1);
};
const convertToObjects = (rows) => {
const headers = ['name', 'age', 'email'];
return rows.map((row) => {
return headers.reduce((obj, header, index) => {
obj[header] = row[index];
return obj;
}, {});
});
};
const filterAdults = (people) => {
return people.filter((person) => parseInt(person.age) >= 18);
};
const addFullName = (people) => {
return people.map((person) => ({
...person,
fullName: person.name.trim(),
}));
};
return Composition.pipe(
parseCSV,
removeHeader,
convertToObjects,
filterAdults,
addFullName
);
}
// Form validation pipeline
static createValidator() {
const required = (value) => {
return value && value.trim().length > 0
? { isValid: true, value }
: { isValid: false, error: 'This field is required' };
};
const minLength = Currying.curry((min, value) => {
return value.length >= min
? { isValid: true, value }
: { isValid: false, error: `Must be at least ${min} characters` };
});
const email = (value) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(value)
? { isValid: true, value }
: { isValid: false, error: 'Invalid email format' };
};
const validateField = (...validators) => {
return (value) => {
for (const validator of validators) {
const result = validator(value);
if (!result.isValid) {
return result;
}
}
return { isValid: true, value };
};
};
return {
required,
minLength,
email,
validateField,
// Pre-configured validators
username: validateField(required, minLength(3)),
password: validateField(required, minLength(8)),
emailField: validateField(required, email),
};
}
}
// Usage examples
const { compose, pipe } = Composition;
const { curry, partial } = Currying;
// Basic composition
const add = (a, b) => a + b;
const multiply = (a, b) => a * b;
const square = (x) => x * x;
const addThenSquare = compose(square, add);
const calculation = pipe(partial(add, 5), partial(multiply, 2), square);
console.log(addThenSquare(3, 4)); // (3 + 4)² = 49
console.log(calculation(3)); // ((3 + 5) * 2)² = 256
// Currying example
const curriedAdd = curry((a, b, c) => a + b + c);
const addFive = curriedAdd(5);
const addFiveAndThree = addFive(3);
console.log(addFiveAndThree(2)); // 5 + 3 + 2 = 10
// Data processing
const processor = FunctionalExamples.createDataProcessor();
const csvData =
'name,age,email\nJohn,25,john@example.com\nJane,17,jane@example.com\nBob,30,bob@example.com';
const processedData = processor(csvData);
console.log(processedData);
// Maybe monad example
const safeDivide = curry((a, b) => {
return b === 0
? FunctionalProgramming.Maybe.nothing()
: FunctionalProgramming.Maybe.of(a / b);
});
const result = FunctionalProgramming.Maybe.of(10)
.flatMap(safeDivide(2)) // 10 / 2 = 5
.map((x) => x * 2) // 5 * 2 = 10
.getOrElse(0);
console.log(result); // 10
Conclusion
Functional programming in JavaScript promotes writing cleaner, more predictable, and testable code. By embracing pure functions, immutability, and function composition, you can build robust applications that are easier to reason about and maintain. Start by incorporating pure functions and immutable data structures into your codebase, then gradually adopt more advanced concepts like currying and function composition as you become comfortable with the paradigm.