Functional Programming Concepts
Functional Programming in JavaScript
Functional programming (FP) is a programming paradigm that treats computation as the evaluation of mathematical functions. While JavaScript is multi-paradigm, it has excellent support for functional programming concepts that lead to more predictable, testable, and maintainable code.
Core Principles
The three pillars of functional programming:
- Pure Functions - Functions with no side effects
- Immutability - Data that never changes
- Function Composition - Building complex functions from simple ones
Pure Functions
A pure function always returns the same output for the same input and has no side effects.
Pure function:
function add(a, b) {
return a + b;
}
console.log(add(2, 3)); // Always 5
console.log(add(2, 3)); // Always 5
Impure function (side effects):
let total = 0;
function addToTotal(value) {
total += value; // Modifies external state
return total;
}
console.log(addToTotal(5)); // 5
console.log(addToTotal(5)); // 10 - different result!
Benefits of pure functions:
- Predictable and easy to test
- Can be cached (memoization)
- Safe to run in parallel
- Easier to debug
Examples:
// Impure - modifies array
function addItemImpure(array, item) {
array.push(item);
return array;
}
// Pure - returns new array
function addItemPure(array, item) {
return [...array, item];
}
const numbers = [1, 2, 3];
const newNumbers = addItemPure(numbers, 4);
console.log(numbers); // [1, 2, 3] - unchanged
console.log(newNumbers); // [1, 2, 3, 4]
Immutability
Once data is created, it never changes. Instead, create new data structures.
Immutable array operations:
const original = [1, 2, 3];
// Add item
const withItem = [...original, 4];
// Remove item
const withoutFirst = original.slice(1);
// Update item
const updated = original.map((item, i) => i === 1 ? 99 : item);
console.log(original); // [1, 2, 3] - never changed
Immutable object operations:
const user = { name: "Alice", age: 25 };
// Add property
const withCity = { ...user, city: "NYC" };
// Update property
const olderUser = { ...user, age: 26 };
// Remove property
const { age, ...withoutAge } = user;
console.log(user); // { name: "Alice", age: 25 } - unchanged
Why immutability matters:
- Prevents bugs from unexpected mutations
- Makes state changes predictable
- Enables time-travel debugging
- Required for React optimization (React.memo, useMemo)
First-Class Functions
In JavaScript, functions are values - you can assign them, pass them around, and return them.
// Assign to variable
const greet = function(name) {
return `Hello, ${name}`;
};
// Pass as argument
function executeFunc(fn, value) {
return fn(value);
}
console.log(executeFunc(greet, "Alice")); // "Hello, Alice"
// Return from function
function createMultiplier(factor) {
return function(number) {
return number * factor;
};
}
const double = createMultiplier(2);
console.log(double(5)); // 10
Higher-Order Functions
Functions that take functions as arguments or return functions.
Array methods are higher-order functions:
const numbers = [1, 2, 3, 4, 5];
// map - transform each item
const doubled = numbers.map(n => n * 2);
// [2, 4, 6, 8, 10]
// filter - select items
const evens = numbers.filter(n => n % 2 === 0);
// [2, 4]
// reduce - combine into single value
const sum = numbers.reduce((total, n) => total + n, 0);
// 15
// Chaining
const result = numbers
.filter(n => n % 2 === 0) // Get evens
.map(n => n * n) // Square them
.reduce((sum, n) => sum + n, 0); // Sum them
// 4 + 16 = 20
Custom higher-order functions:
// Function that creates validators
function createValidator(test, message) {
return function(value) {
if (!test(value)) {
throw new Error(message);
}
return value;
};
}
const validateEmail = createValidator(
email => email.includes("@"),
"Invalid email"
);
const validateAge = createValidator(
age => age >= 18,
"Must be 18 or older"
);
// Use validators
try {
validateEmail("alice@example.com"); // OK
validateEmail("invalid"); // Throws error
} catch (error) {
console.log(error.message);
}
Function Composition
Combine simple functions to create complex ones.
// Simple functions
const double = x => x * 2;
const addOne = x => x + 1;
const square = x => x * x;
// Manual composition
const result = square(addOne(double(3))); // ((3 * 2) + 1)² = 49
// Compose helper
const compose = (...fns) => (value) =>
fns.reduceRight((acc, fn) => fn(acc), value);
const compute = compose(square, addOne, double);
console.log(compute(3)); // 49
Practical composition:
// Data transformations
const users = [
{ name: "Alice", age: 25, active: true },
{ name: "Bob", age: 17, active: true },
{ name: "Charlie", age: 30, active: false }
];
const isActive = user => user.active;
const isAdult = user => user.age >= 18;
const getName = user => user.name;
const getActiveAdultNames = compose(
users => users.map(getName),
users => users.filter(isActive),
users => users.filter(isAdult)
);
console.log(getActiveAdultNames(users)); // ["Alice"]
Currying
Transform a function with multiple arguments into a sequence of functions with single arguments.
// Regular function
function add(a, b, c) {
return a + b + c;
}
// Curried function
function addCurried(a) {
return function(b) {
return function(c) {
return a + b + c;
};
};
}
// Or with arrows
const addCurried = a => b => c => a + b + c;
// Usage
console.log(addCurried(1)(2)(3)); // 6
// Partial application
const add5 = addCurried(5);
const add5and10 = add5(10);
console.log(add5and10(3)); // 18
Practical currying:
// Logger with levels
const log = level => message => {
console.log(`[${level}] ${new Date().toISOString()}: ${message}`);
};
const logInfo = log("INFO");
const logError = log("ERROR");
logInfo("User logged in");
logError("Database connection failed");
Practical Examples
Example 1: Data Pipeline
// Transform and filter data
const products = [
{ name: "Laptop", price: 1000, category: "Electronics", inStock: true },
{ name: "Phone", price: 500, category: "Electronics", inStock: false },
{ name: "Desk", price: 300, category: "Furniture", inStock: true }
];
const processProducts = products =>
products
.filter(p => p.inStock)
.filter(p => p.price < 600)
.map(p => ({
name: p.name,
displayPrice: `$${p.price}`,
category: p.category
}))
.sort((a, b) => a.name.localeCompare(b.name));
console.log(processProducts(products));
// [{ name: "Desk", displayPrice: "$300", category: "Furniture" }]
Example 2: State Management (React-style)
// Immutable state updates
const initialState = {
user: null,
posts: [],
loading: false
};
// Pure reducer
function reducer(state, action) {
switch (action.type) {
case "SET_USER":
return { ...state, user: action.payload };
case "ADD_POST":
return { ...state, posts: [...state.posts, action.payload] };
case "SET_LOADING":
return { ...state, loading: action.payload };
default:
return state;
}
}
let state = initialState;
state = reducer(state, { type: "SET_USER", payload: { name: "Alice" } });
state = reducer(state, { type: "ADD_POST", payload: { id: 1, title: "First" } });
console.log(state);
// { user: { name: "Alice" }, posts: [{ id: 1, title: "First" }], loading: false }
Example 3: Validation Pipeline
// Composable validators
const required = field => value => {
if (!value) throw new Error(`${field} is required`);
return value;
};
const minLength = (field, min) => value => {
if (value.length < min) {
throw new Error(`${field} must be at least ${min} characters`);
}
return value;
};
const isEmail = field => value => {
if (!value.includes("@")) {
throw new Error(`${field} must be a valid email`);
}
return value;
};
// Compose validators
const validateUsername = compose(
minLength("Username", 3),
required("Username")
);
const validateEmail = compose(
isEmail("Email"),
required("Email")
);
// Use
try {
validateUsername("Al"); // Throws: Username must be at least 3 characters
} catch (error) {
console.log(error.message);
}
Example 4: Memoization
// Cache expensive computations
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;
};
}
const expensiveOperation = memoize((n) => {
console.log("Computing...");
return n * n;
});
console.log(expensiveOperation(5)); // "Computing..." 25
console.log(expensiveOperation(5)); // 25 (cached, no log)
Declarative vs Imperative
Functional programming favors declarative style (what to do) over imperative (how to do it).
Imperative (how):
const numbers = [1, 2, 3, 4, 5];
const doubled = [];
for (let i = 0; i < numbers.length; i++) {
doubled.push(numbers[i] * 2);
}
Declarative (what):
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2);
Benefits in Modern JavaScript
React and functional programming:
- Components are pure functions
- Props → UI is predictable
- Immutable state updates
- Hooks embrace functional patterns
// React component as pure function
function UserCard({ name, age, avatar }) {
return (
<div className="user-card">
<img src={avatar} alt={name} />
<h3>{name}</h3>
<p>Age: {age}</p>
</div>
);
}
// State updates must be immutable
const [items, setItems] = useState([]);
setItems(prev => [...prev, newItem]); // Don't mutate!
Best Practices
- Prefer pure functions when possible
- Avoid mutations - use spread, map, filter instead of push, splice
- Use array methods over loops
- Keep functions small and single-purpose
- Name functions clearly to show intent
- Compose small functions into larger ones
Key Takeaways
- Pure functions = predictable, testable code
- Immutability = fewer bugs, easier debugging
- Composition = build complex from simple
- Higher-order functions = powerful abstractions
- Declarative code = more readable, maintainable
Functional programming in JavaScript isn’t all-or-nothing. Use these concepts where they make sense, and your code will be cleaner and more maintainable.
Learn more in the Browser JavaScript section