
Functional Programming: Pure Functions & Immutability
Cooking Class (OOP) vs Math Class (FP). Eliminating bugs by rejecting State.

Cooking Class (OOP) vs Math Class (FP). Eliminating bugs by rejecting State.
Why does my server crash? OS's desperate struggle to manage limited memory. War against Fragmentation.

Two ways to escape a maze. Spread out wide (BFS) or dig deep (DFS)? Who finds the shortest path?

Fast by name. Partitioning around a Pivot. Why is it the standard library choice despite O(N²) worst case?

Establishing TCP connection is expensive. Reuse it for multiple requests.

When I started coding, I felt a strange anxiety. The more code I wrote, the more features I added, the more nervous I became. "If I change this variable here... won't it break that function over there?" This thought haunted me. Debugging was worse. To find a bug, I had to search through the entire codebase. Somewhere, a variable changed, triggering a chain reaction.
Then I encountered Functional Programming (FP). At first, I thought, "What is this? Why write code in such an inconvenient way?" Don't mutate variables? Copy everything? Isn't that inefficient? But after actually using it, I realized this wasn't inefficiency. It was a trade: stability in exchange for convenience.
I first understood functional programming through this metaphor.
In a cooking class, the teacher says, "Add a spoon of salt to the kimchi stew." I add salt (method call) to the pot (object). The stew's taste (state) changes. That's the essence of OOP. Objects have state, and methods mutate that state.
Here's the problem. If someone secretly adds sugar, the stew is ruined. It's hard to track who added salt, who added sugar, and when. If multiple people (threads) touch the same pot (shared state) simultaneously, chaos ensues. That's a Side Effect.
In math class, the teacher says, "We have a function f(x) = x + 1. If x is 3, what's the output?" Obviously, 4. Ask 100 times, it's still 4. Ask tomorrow, still 4. This function never changes.
The key point: 3 doesn't become 4. The number 3 stays 3. The function simply creates a new value, 4. That's the essence of functional programming. Don't mutate input; create new output.
This difference seemed trivial at first. "So what?" But as code complexity grew, this difference became massive.
As a beginner, I often wrote code like this:
let cart = [];
function addItem(item) {
cart.push(item);
updateUI();
saveToLocalStorage();
}
function removeItem(index) {
cart.splice(index, 1);
updateUI();
saveToLocalStorage();
}
function applyDiscount() {
cart.forEach(item => {
item.price = item.price * 0.9; // Mutates original
});
updateUI();
}
Looks fine on the surface. But in practice, it was a disaster. The global variable cart gets mutated everywhere. Calling applyDiscount() permanently changes item prices. Want to undo the discount? How do you restore the previous state? You should've saved it, but forgot.
The bigger issue was unpredictability. If cart changes somewhere, the impact spreads throughout the app. When bugs appeared, I wasted hours tracking "Where exactly did this value change?"
The first principle I learned from functional programming was Immutability. Don't mutate variables. Instead, copy and create new values.
const cart = [];
function addItem(cart, item) {
return [...cart, item]; // Keep original, return new array
}
function removeItem(cart, index) {
return cart.filter((_, i) => i !== index);
}
function applyDiscount(cart) {
return cart.map(item => ({
...item,
price: item.price * 0.9 // Create new object
}));
}
// Usage
let myCart = [];
myCart = addItem(myCart, { name: 'Book', price: 10000 });
myCart = addItem(myCart, { name: 'Pen', price: 2000 });
const discountedCart = applyDiscount(myCart);
// myCart unchanged, discountedCart is new array with discount applied
Initially, I thought, "Isn't this more inefficient?" Copying everything uses more memory and seems slower. But in practice, the advantages were overwhelming.
After embracing this, coding became much easier. The anxiety disappeared.
The second core of functional programming is Pure Functions. Pure functions satisfy two conditions:
// Pure function
function add(a, b) {
return a + b;
}
// Impure function (side effect)
let total = 0;
function addToTotal(value) {
total += value; // Mutates external variable
return total;
}
// Impure function (external dependency)
function getCurrentTime() {
return new Date().getTime(); // Returns different value each time
}
// Impure function (I/O)
function saveToDatabase(data) {
db.save(data); // Mutates external system
}
I wondered, "Then how do I do I/O? Database saves? API calls?" The answer was simple: Separate pure and impure parts.
// Pure business logic
function calculateOrderTotal(items) {
return items.reduce((sum, item) => sum + item.price, 0);
}
function applyTax(total, taxRate) {
return total * (1 + taxRate);
}
// Impure I/O part
async function processOrder(items) {
const total = calculateOrderTotal(items); // Pure
const finalTotal = applyTax(total, 0.1); // Pure
await saveToDatabase({ items, finalTotal }); // Impure (but isolated)
}
This way, business logic (calculateOrderTotal, applyTax) is pure and easy to test, while I/O is minimally isolated.
In functional programming, functions aren't special. They're values like numbers and strings. You can store them in variables, pass them as arguments, and return them from functions. This is called First-Class Functions.
// Assign function to variable
const greet = function(name) {
return `Hello, ${name}`;
};
// Pass function as argument
function executeFunc(func, value) {
return func(value);
}
executeFunc(greet, 'Alice'); // "Hello, Alice"
// Function returns function
function makeMultiplier(factor) {
return function(number) {
return number * factor;
};
}
const double = makeMultiplier(2);
double(5); // 10
Higher-Order Functions are functions that take functions as arguments or return functions. JavaScript's array methods (map, filter, reduce) are prime examples.
I used to write for-loops, but after learning these methods, my code became much more readable.
// Imperative style
const numbers = [1, 2, 3, 4, 5];
const doubled = [];
for (let i = 0; i < numbers.length; i++) {
doubled.push(numbers[i] * 2);
}
// Declarative style (functional)
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2);
Imperative code describes "How" to do it. Loops, indices, push... complex. Declarative code describes "What" to do. "Double each element." The intent is clear.
The real power of functional programming shines in Function Composition. Small pure functions combine like Lego blocks to create complex logic.
// Small pure functions
const users = [
{ name: 'Alice', age: 25, premium: true },
{ name: 'Bob', age: 17, premium: false },
{ name: 'Charlie', age: 30, premium: true },
{ name: 'David', age: 16, premium: false },
];
// Imperative approach
function getAdultPremiumUserNames(users) {
const result = [];
for (let i = 0; i < users.length; i++) {
if (users[i].age >= 18 && users[i].premium) {
result.push(users[i].name);
}
}
return result;
}
// Functional approach
const isAdult = user => user.age >= 18;
const isPremium = user => user.premium;
const getName = user => user.name;
const adultPremiumNames = users
.filter(isAdult)
.filter(isPremium)
.map(getName);
// ['Alice', 'Charlie']
I much prefer this approach. Each function (isAdult, isPremium, getName) can be tested independently. Easy to reuse. Easy to read. "Filter adults, filter premium users, extract names." The code is self-documenting.
Going further, you can use utilities like compose or pipe.
// compose: executes right to left
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
// pipe: executes left to right (more intuitive)
const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);
// Example
const addOne = x => x + 1;
const double = x => x * 2;
const square = x => x * x;
const compute = pipe(addOne, double, square);
compute(2); // ((2 + 1) * 2)^2 = 36
// Data pipeline
const processUsers = pipe(
users => users.filter(isAdult),
users => users.filter(isPremium),
users => users.map(getName),
names => names.join(', ')
);
processUsers(users); // "Alice, Charlie"
This works like Unix pipelines (cat file.txt | grep "error" | wc -l). Data flows through functions, transforming along the way. After using this pattern, I found complex data processing logic became much cleaner.
Currying transforms a multi-argument function into a chain of single-argument functions.
// Normal function
function add(a, b, c) {
return a + b + c;
}
add(1, 2, 3); // 6
// Curried function
function curriedAdd(a) {
return function(b) {
return function(c) {
return a + b + c;
};
};
}
curriedAdd(1)(2)(3); // 6
// Concise with arrow functions
const curriedAdd = a => b => c => a + b + c;
At first, I thought, "Why so complicated?" But Partial Application makes it powerful.
const add = a => b => c => a + b + c;
const add5 = add(5); // b => c => 5 + b + c
const add5and10 = add5(10); // c => 5 + 10 + c
add5and10(3); // 18
// Practical example
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]
This pattern is useful for pre-configuring functions. For example, creating logging functions:
const log = level => message => timestamp =>
`[${timestamp}] ${level}: ${message}`;
const infoLog = log('INFO');
const errorLog = log('ERROR');
infoLog('Server started')('2025-01-01 10:00:00');
// "[2025-01-01 10:00:00] INFO: Server started"
After learning this, I started using it frequently for configuration functions.
Using React made me appreciate functional programming even more. React's functional components are literally pure functions.
// Pure functional component
function UserCard({ name, age, email }) {
return (
<div className="card">
<h2>{name}</h2>
<p>Age: {age}</p>
<p>Email: {email}</p>
</div>
);
}
// Same props always produce same UI
React's useState, useEffect, etc. are based on functional concepts. Immutability is required for re-rendering to work properly.
// Wrong way (violates immutability)
const [items, setItems] = useState([1, 2, 3]);
items.push(4); // Mutates original - React won't detect
setItems(items); // No re-render!
// Right way (maintains immutability)
setItems([...items, 4]); // Creates new array - React detects
Libraries like Immer make immutability easier.
import produce from 'immer';
const [state, setState] = useState({ cart: [], total: 0 });
// Using Immer
setState(produce(draft => {
draft.cart.push({ id: 1, name: 'Book' }); // Looks like mutation
draft.total += 10000; // But actually immutable update
}));
Closures allow functions to remember the environment (lexical scope) where they were created. In functional programming, closures are essential.
function createCounter() {
let count = 0; // Inaccessible from outside (encapsulation)
return {
increment: () => ++count,
decrement: () => --count,
getValue: () => count
};
}
const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
counter.getValue(); // 2
// Can't access count directly - safe
I found it fascinating that closures enable private variables. JavaScript didn't originally have a private keyword, but closures achieve similar effects.
Studying functional programming, you encounter the intimidating term "Monad." The mathematical definition is complex, but I understood it this way: A Monad is a container that wraps values for safe handling.
The most familiar Monad in JavaScript is Promise.
// Promise is a Monad wrapping async values
fetch('/api/user')
.then(response => response.json())
.then(data => data.name)
.then(name => console.log(name))
.catch(error => console.error(error));
// Similar pattern to map
// Array: [1, 2, 3].map(x => x * 2)
// Promise: Promise.resolve(3).then(x => x * 2)
Promise safely handles values that might fail (errors). There's also the Maybe Monad for handling null or undefined.
// Maybe Monad (simple implementation)
class Maybe {
constructor(value) {
this.value = value;
}
static of(value) {
return new Maybe(value);
}
map(fn) {
return this.value == null ? this : Maybe.of(fn(this.value));
}
getOrElse(defaultValue) {
return this.value == null ? defaultValue : this.value;
}
}
// Usage
const user = Maybe.of({ name: 'Alice', age: 25 });
const userName = user
.map(u => u.name)
.map(name => name.toUpperCase())
.getOrElse('UNKNOWN'); // "ALICE"
const nullUser = Maybe.of(null);
const nullUserName = nullUser
.map(u => u.name) // Skipped because null
.getOrElse('UNKNOWN'); // "UNKNOWN"
I can't claim complete understanding of Monads, but I accepted them as "patterns for safely handling errors or null." In practice, just using Promises well is enough.
I often wondered, "FP sounds good, but should I abandon OOP?" The answer: Use both.
In practice, you usually mix them:
// OOP for domain modeling
class User {
constructor(name, email, age) {
this.name = name;
this.email = email;
this.age = age;
}
}
// FP for data processing
const users = [
new User('Alice', 'alice@example.com', 25),
new User('Bob', 'bob@example.com', 17),
];
const getAdultEmails = users =>
users
.filter(u => u.age >= 18)
.map(u => u.email);
getAdultEmails(users); // ['alice@example.com']
I realized finding this balance is important. No need to become an FP zealot. Use what fits the situation.
Applying functional programming in real work, I noticed these practical benefits:
Pure functions just need input/output verification. No mocks needed.
// Pure function - easy to test
function calculateDiscount(price, discountRate) {
return price * (1 - discountRate);
}
test('discount calculation', () => {
expect(calculateDiscount(10000, 0.1)).toBe(9000);
expect(calculateDiscount(5000, 0.2)).toBe(4000);
});
// Impure function - hard to test
function applyDiscountToCart() {
const cart = getCartFromDB(); // DB dependency
const discount = getCurrentDiscount(); // External state dependency
cart.total = cart.total * discount;
saveCartToDB(cart); // DB mutation
}
// Testing this requires DB mocks, state mocks... complex
Pure functions always produce the same output for the same input. Easy to reproduce.
// Easy bug reproduction
function processOrder(items, taxRate) {
const total = calculateTotal(items);
return applyTax(total, taxRate);
}
// When bug found
processOrder([{ price: 1000 }], 0.1); // Always reproducible with this input
Immutable data is safe for multiple threads to read simultaneously. No race conditions.
// Immutable approach - safe
const data = [1, 2, 3];
Promise.all([
processData(data), // Original unchanged
processData(data), // Original unchanged
]);
// Mutable approach - dangerous
let data = [1, 2, 3];
Promise.all([
mutateData(data), // Mutates data
mutateData(data), // Concurrent mutation? Collision!
]);
After learning functional programming, my anxiety about code decreased significantly. Instead of worrying "What happens if I change this variable here?", I gained confidence that "This function always produces the same result."
Functional programming isn't magic. It uses more memory and sometimes performs slightly worse. But in exchange, you get predictability, testability, and stability. In modern development, these values are far more important than a few milliseconds of performance.
Ultimately, functional programming is one answer to "How do you manage state?" Minimize state, don't mutate it, copy and create new values. Following this principle alone makes code much more robust. Now when I declare a variable, I first ask "Does this really need to change?" That's the biggest change functional programming brought to me.