Functional Programming: Simple Concepts, Powerful Results
Why Functional Programming?
In a world of increasingly complex applications, functional programming offers a refreshing approach: write functions that do one thing, do it well, and compose them together.
The benefits:
- Easier to test (pure functions have predictable outputs)
- Easier to reason about (no hidden state mutations)
- Better code reuse (composable functions)
- Natural parallelization (no shared mutable state)
Core Concepts
1. Pure Functions
A pure function always returns the same output for the same input and has no side effects.
// ❌ IMPURE: depends on external state
let multiplier = 2;
function multiply(x) {
return x * multiplier; // multiplier could change!
}
// ✅ PURE: all inputs as parameters
function multiply(x, multiplier) {
return x * multiplier;
}
// ❌ IMPURE: modifies external state
const user = { name: 'John', age: 30 };
function incrementAge(user) {
user.age++; // mutation!
return user;
}
// ✅ PURE: returns new object
function incrementAge(user) {
return { ...user, age: user.age + 1 };
} 2. Immutability
Data never changes; instead, you create new versions with changes applied.
// Arrays
const original = [1, 2, 3];
const modified = [...original, 4]; // [1, 2, 3, 4]
const removed = original.filter(x => x !== 2); // [1, 3]
// Objects
const person = { name: 'Alice', age: 25 };
const olderPerson = { ...person, age: 26 };
// Never: person.age = 26 (mutation!)
// Deep immutability
const nested = { user: { name: 'Bob' } };
const updated = {
...nested,
user: { ...nested.user, name: 'Charlie' }
}; 3. First-Class Functions
Functions are treated as values—pass them around, return them, store them.
// Functions as arguments (callbacks, higher-order functions)
function applyTwice(fn, value) {
return fn(fn(value));
}
applyTwice(x => x * 2, 5); // 20
// Returning functions
function makeAdder(x) {
return y => x + y;
}
const add5 = makeAdder(5);
add5(3); // 8
// Higher-order functions
function map(fn, array) {
return array.map(fn);
}
function filter(predicate, array) {
return array.filter(predicate);
} 4. Function Composition
Combine simple functions into complex operations.
// The compose utility
function compose(...fns) {
return x => fns.reduceRight((acc, fn) => fn(acc), x);
}
// Building blocks
const double = x => x * 2;
const addOne = x => x + 1;
const square = x => x * x;
// Compose them
const operation = compose(square, addOne, double);
operation(3); // square(addOne(double(3))) = square(addOne(6)) = square(7) = 49
// Pipe (left-to-right, more readable)
function pipe(...fns) {
return x => fns.reduce((acc, fn) => fn(acc), x);
}
const operation2 = pipe(double, addOne, square);
operation2(3); // ((3 * 2) + 1) ^ 2 = 49 Real-World Examples
Data Transformation Pipeline
// Typical imperative approach
function processUsers(users) {
const result = [];
for (let i = 0; i < users.length; i++) {
const user = users[i];
if (user.age >= 18) {
const processed = {
...user,
name: user.name.toUpperCase(),
category: user.age > 65 ? 'senior' : 'adult'
};
result.push(processed);
}
}
return result;
}
// Functional approach
const isAdult = user => user.age >= 18;
const categorizeAge = user => ({
...user,
category: user.age > 65 ? 'senior' : 'adult'
});
const uppercaseName = user => ({
...user,
name: user.name.toUpperCase()
});
const processUsers = pipe(
arr => arr.filter(isAdult),
arr => arr.map(uppercaseName),
arr => arr.map(categorizeAge)
);
// Or chained:
const processUsers = users =>
users
.filter(isAdult)
.map(uppercaseName)
.map(categorizeAge); Error Handling with Monads
// Simple Result monad
class Result {
constructor(value, isError = false) {
this.value = value;
this.isError = isError;
}
static success(value) {
return new Result(value, false);
}
static error(error) {
return new Result(error, true);
}
map(fn) {
return this.isError ? this : Result.success(fn(this.value));
}
flatMap(fn) {
return this.isError ? this : fn(this.value);
}
getOrElse(defaultValue) {
return this.isError ? defaultValue : this.value;
}
}
// Usage
const divideBy = divisor => value => {
if (divisor === 0) return Result.error('Division by zero');
return Result.success(value / divisor);
};
const result = Result.success(20)
.flatMap(divideBy(4))
.flatMap(divideBy(2))
.map(x => x + 5);
console.log(result.getOrElse(0)); // 7.5
// With error
const badResult = Result.success(20)
.flatMap(divideBy(0)) // error!
.flatMap(divideBy(2)); // skipped
console.log(badResult.getOrElse(0)); // 0 Currying and Partial Application
// Currying: function takes one argument, returns function waiting for next
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn(...args);
}
return (...nextArgs) => curried(...args, ...nextArgs);
};
}
const add = (a, b, c) => a + b + c;
const curriedAdd = curry(add);
curriedAdd(1)(2)(3); // 6
curriedAdd(1, 2)(3); // 6
curriedAdd(1)(2, 3); // 6
// Practical: API request builder
const fetchUser = curry((baseUrl, userId, token) => {
return fetch(`${baseUrl}/users/${userId}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
});
const fetchFromApi = fetchUser('https://api.example.com');
const fetchUser123 = fetchFromApi('123');
const request = fetchUser123('my-token'); Immutable Data Structures
// Using Immer for immutability
import produce from 'immer';
const state = {
users: [
{ id: 1, name: 'Alice', posts: [{ id: 1, title: 'Hello' }] }
]
};
// Clean, readable mutation-like syntax
// Immer ensures immutability under the hood
const newState = produce(state, draft => {
draft.users[0].posts.push({ id: 2, title: 'World' });
});
console.log(state === newState); // false (new object)
console.log(state.users[0] === newState.users[0]); // false Avoiding Common Pitfalls
// ❌ Don't: overly nested compositions
pipe(
arr => arr.filter(x => x > 5),
arr => arr.map(x => x * 2),
arr => arr.reduce((a, b) => a + b, 0)
);
// ✅ Do: break it into readable steps
const filterLarge = arr => arr.filter(x => x > 5);
const doubleAll = arr => arr.map(x => x * 2);
const sum = arr => arr.reduce((a, b) => a + b, 0);
const pipeline = pipe(filterLarge, doubleAll, sum);
// ❌ Don't: lose context with arrow functions
const user = {
name: 'John',
greet() {
setTimeout(() => {
console.log(`Hello, I'm ${this.name}`); // ✅ works due to arrow fn
}, 100);
}
};
// ❌ Don't: forget that FP still needs statements
const result = x => {
if (x > 10) return 'big';
return 'small';
};
// ✅ Do: or use ternary for simple cases
const result = x => x > 10 ? 'big' : 'small'; Functional JavaScript Patterns in Practice
Redux / State Management
// Pure reducer
const initialState = { count: 0 };
function counterReducer(state = initialState, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'ADD':
return { ...state, count: state.count + action.payload };
default:
return state;
}
}
// Composable action creators
const increment = () => ({ type: 'INCREMENT' });
const add = payload => ({ type: 'ADD', payload }); React with Functional Patterns
// Composition over inheritance
const withLogging = Component => props => {
useEffect(() => console.log(`Mounted: ${Component.name}`), []);
return <Component {...props} />;
};
// Pure components are naturally testable
const UserCard = ({ user, onDelete }) => (
<div>
<h3>{user.name}</h3>
<button onClick={() => onDelete(user.id)}>Delete</button>
</div>
);
// Testing is trivial
test('calls onDelete when clicked', () => {
const mock = jest.fn();
render(<UserCard user={{ id: 1, name: 'Test' }} onDelete={mock} />);
fireEvent.click(screen.getByText('Delete'));
expect(mock).toHaveBeenCalledWith(1);
}); Conclusion
Functional programming isn’t about using map, filter, and reduce everywhere. It’s about:
- Thinking in transformations - data flows through functions
- Avoiding side effects - pure functions are predictable
- Composing small pieces - complex logic from simple building blocks
- Embracing immutability - no hidden state changes
Start small: write pure functions, use const, and compose where it makes sense. You don’t need to go all-in functional to reap the benefits.
Further Resources
- “Composing Software” by Eric Elliott
- RxJS for reactive functional programming
- Ramda.js for functional utilities
- Clojure/Haskell to learn “pure” functional languages
How do you use functional programming in your projects? Share your patterns in the comments!