Functional Programming as Stoic Philosophy: Immutability and Accepting What You Cannot Change
Marcus Aurelius, Roman Emperor and Stoic philosopher, wrote in his Meditations: “You have power over your mind—not outside events. Realize this, and you will find strength.”
Two thousand years later, functional programmers write: “You have power over new values—not existing data. Realize this, and you will find clarity.”
The parallel isn’t coincidental. Functional programming embodies philosophical principles that the Stoics articulated centuries ago: distinguish what you can control from what you cannot, accept immutable reality rather than fighting it, focus on transformation over manipulation, and find freedom through constraint.
Both traditions offer the same radical insight: our suffering comes not from circumstances themselves, but from our futile attempts to change what cannot be changed.
1. The Dichotomy of Control: What’s Yours to Change
The foundation of Stoic philosophy is the dichotomy of control—dividing the world into what you can control (your thoughts, actions, and responses) and what you cannot (external events, other people’s opinions, the past).
Epictetus taught: “Some things are within our power, while others are not. Within our power are opinion, motivation, desire, aversion, and, in a word, whatever is of our own doing; not within our power are our body, our property, reputation, office, and, in a word, whatever is not of our own doing.”
Functional programming implements this exact distinction through immutability. You cannot change existing data structures—they’re outside your control, like external events. You can only create new values—that’s within your control, like your response to circumstances.
Consider this imperative approach:
function addItem(cart, item) {
cart.items.push(item);
cart.total += item.price;
return cart;
}
This code attempts to control what should be immutable. It reaches into the cart object and manipulates it directly—the programming equivalent of trying to change the past or control external events.
The functional approach accepts what exists:
function addItem(cart, item) {
return {
...cart,
items: [...cart.items, item],
total: cart.total + item.price
};
}
The original cart remains unchanged. You cannot control it. You can only create a new reality—a new cart—that reflects your desired transformation.
This isn’t just style preference. It’s a philosophical stance: accept what is, create what will be.
2. Immutability as Acceptance: You Cannot Change the Past
The Stoics emphasized accepting reality as it is. Marcus Aurelius wrote: “Accept the things to which fate binds you, and love the people with whom fate brings you together, but do so with all your heart.”
Immutability is acceptance formalized in code. When data is immutable, you’re forced to acknowledge: this value exists as it is. You cannot change history. The past is fixed.
This creates psychological freedom. In imperative programming, you constantly fight reality:
“This object has the wrong state—I’ll change it.”
“This array contains bad data—I’ll modify it.”
“This variable has an unexpected value—I’ll update it.”
Each statement expresses struggle against what is. You’re perpetually trying to fix reality rather than accepting it and creating something new.
Functional programming offers different psychology:
“This data exists as it is—I’ll transform it into what I need.”
“The current state is given—I’ll produce the next state.”
“Reality is immutable—I’ll create my response.”
The Clojure programming language makes this explicit. Rich Hickey, its creator, says: “Most things in life are immutable. We need to represent them that way.” Your birth date is immutable. Historical events are immutable. Yesterday’s temperature is immutable. Code should reflect this reality.
When you try to mutate data, you’re fighting against the nature of time itself. You’re pretending the present can reach backward and alter the past. Functional programming aligns with how reality actually works: one moment follows another, each immutable, each giving rise to the next.
3. Pure Functions as Virtue: Control Your Response
Stoics taught that virtue lies in controlling your response to circumstances, not in controlling circumstances themselves. Seneca wrote: “It is not because things are difficult that we do not dare; it is because we do not dare that things are difficult.”
In functional programming, pure functions embody this principle. A pure function controls its response—its output—based on its input. It doesn’t manipulate external state, produce side effects, or depend on anything beyond what’s explicitly provided.
Consider an impure function:
let total = 0;
function addToTotal(amount) {
total += amount;
updateDatabaseTotal(total);
sendNotificationEmail();
}
This function attempts to control the external world. It manipulates global state, touches databases, sends emails—all actions that reach beyond its immediate responsibility. It’s like the person who tries to control everything around them, creating chaos and unpredictability.
A pure function accepts its constraints:
function calculateNewTotal(currentTotal, amount) {
return currentTotal + amount;
}
This function controls only what it should: given inputs, it produces an output. All else is outside its domain. This is Stoic discipline applied to code.
The benefit isn’t just philosophical—it’s practical. Pure functions are predictable, testable, and composable. Just as Stoic individuals are reliable and trustworthy because they control only themselves, pure functions are reliable and trustworthy because they control only their outputs.
4. Side Effects as Passion: The Disruption of Control
The Stoics distinguished between passion (uncontrolled emotion) and proper feeling (appropriate response). Passions—fear, anger, desire—disrupt rational judgment and lead to suffering. The goal wasn’t eliminating emotion but controlling your response to emotional triggers.
In functional programming, side effects are passions—uncontrolled actions that disrupt the flow of logic.
A side effect occurs when a function does something beyond computing a return value: modifying global state, writing to disk, making network calls, logging to console, updating the DOM. These actions reach beyond the function’s boundaries and entangle it with the external world.
Consider this entangled code:
function processOrder(order) {
const validated = validateOrder(order);
saveToDatabase(validated);
const total = calculateTotal(validated);
sendConfirmationEmail(validated);
updateInventory(validated.items);
logOrderEvent(validated);
return total;
}
This function is governed by passion—it carelessly reaches into databases, email systems, inventory, and logs. Each action creates unpredictability. What happens if email fails? What if database is down? The function lacks self-control.
The Stoic functional approach separates concerns:
function processOrder(order) {
const validated = validateOrder(order);
const total = calculateTotal(validated);
return {
validated,
total,
effects: [
{ type: 'DATABASE_SAVE', data: validated },
{ type: 'EMAIL_SEND', data: validated },
{ type: 'INVENTORY_UPDATE', data: validated.items },
{ type: 'LOG_EVENT', data: validated }
]
};
}
The function controls its response—it computes what needs to happen. But it doesn’t execute those passions directly. It returns a description of effects, leaving execution to a system designed for that purpose.
This is exactly the Stoic approach: recognize what external actions are necessary, but maintain self-control in how you engage them. Don’t let passions rule—describe them, acknowledge them, but execute them deliberately.
5. Composition Over Inheritance: Accepting Your Nature
Stoicism teaches accepting your nature while striving for excellence within it. Epictetus said: “Don’t demand that things happen as you wish, but wish that they happen as they do happen, and you will go on well.”
In programming, this manifests in the composition over inheritance principle. Object-oriented inheritance tries to force entities into rigid hierarchies—”a square IS-A rectangle,” creating contradictions when squares can’t behave like rectangles.
Functional composition accepts what each piece is and combines them:
const addTax = total => total * 1.08;
const addShipping = total => total + 5.99;
const applyDiscount = (total, rate) => total * (1 - rate);
const calculateFinalPrice = (subtotal, discountRate) =>
addShipping(addTax(applyDiscount(subtotal, discountRate)));
Each function accepts what it receives and produces what it can. No function tries to be something it’s not. They combine through composition—accepting each other’s nature and working together.
This mirrors Stoic social philosophy: accept people as they are, work with their nature, and create harmony through collaboration rather than trying to change fundamental character.
6. Declarative Thinking: Focus on the What, Not the How
Marcus Aurelius advised: “You have power over your mind—not outside events.” Stoics focused on intentions and principles rather than specific mechanisms of execution.
Declarative programming embodies this focus. You declare what you want, not how to achieve it. You describe the desired outcome and trust the system to figure out execution.
Compare imperative (focusing on how):
function getAdultNames(users) {
const results = [];
for (let i = 0; i < users.length; i++) {
if (users[i].age >= 18) {
results.push(users[i].name);
}
}
return results;
}
With declarative (focusing on what):
const getAdultNames = users =>
users
.filter(user => user.age >= 18)
.map(user => user.name);
The declarative version states your intention clearly: filter for adults, extract names. The imperative version gets lost in mechanics: create array, loop through indices, check conditions, push to array.
Stoics would recognize this distinction. Getting caught in the “how”—the mechanics of external events—causes suffering. Focusing on the “what”—your clear intentions—brings clarity.
SQL exemplifies this philosophical stance. You declare what data you want:
SELECT name FROM users WHERE age >= 18;
You don’t specify how to scan the table, whether to use indices, or in what order to apply filters. Those are outside your control. You control only your intention—the what.
7. Referential Transparency: Truth Independent of Context
Stoics valued apatheia—freedom from passion that comes from aligning your will with nature and reason. This wasn’t coldness but clarity—seeing things as they truly are, independent of your emotional reactions.
In functional programming, referential transparency provides this clarity. An expression is referentially transparent if you can replace it with its value without changing program behavior:
const result = calculateTotal(cart); // is identical to const result = 149.99;
If calculateTotal(cart) always returns 149.99 for a given cart, you can substitute one for the other. The expression’s meaning is independent of context—it’s a fact, not an opinion.
Imperative code lacks this property:
function getNextId() {
return ++globalCounter;
}
const id1 = getNextId(); // 1
const id2 = getNextId(); // 2
You cannot replace getNextId() with a value because it depends on hidden state and context. It’s like an emotional reaction—unpredictable and context-dependent rather than a clear truth.
Referential transparency mirrors Stoic detachment from circumstances. A pure function’s output depends only on inputs—external context cannot disturb it. Similarly, a Stoic’s virtue depends only on intention—external circumstances cannot disturb it.
8. Monads as Acceptance of Uncertainty
One of functional programming’s most philosophical constructs is the monad—particularly the Maybe/Option monad for handling absence.
Stoics taught accepting uncertainty and preparing for various outcomes. Seneca practiced premeditatio malorum—visualizing potential misfortunes to reduce their emotional impact.
The Maybe monad formalizes this practice in code. Instead of pretending values always exist, you accept that they might not:
// Imperative: pretend everything exists
function getUserEmail(userId) {
const user = database.getUser(userId);
return user.email.toLowerCase();
// Crashes if user is null or email is undefined
}
This code exhibits false confidence—assuming reality will conform to hopes.
The Stoic functional approach accepts uncertainty:
function getUserEmail(userId) {
return database.getUser(userId)
.flatMap(user => user.email)
.map(email => email.toLowerCase());
}
This code acknowledges: the user might not exist, the email might not exist, and we’ll handle each case appropriately. We’re prepared for reality as it is, not as we wish it to be.
This mirrors Stoic practice perfectly. Hope for the best, prepare for various outcomes, and maintain equanimity regardless of what happens.
9. Recursion as Acceptance of Process
Stoics emphasized amor fati—love of fate. Accept that life is a process unfolding according to natural law. Each moment leads to the next in an inevitable chain.
Recursion embodies this acceptance. Instead of imposing control through loops and mutations, recursion accepts the process: each step naturally gives rise to the next.
Compare imperative iteration:
function factorial(n) {
let result = 1;
for (let i = 1; i <= n; i++) {
result *= i;
}
return result;
}
With recursive acceptance:
function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}
The recursive version accepts the nature of factorial: it emerges from the relationship between a number and its predecessor. We don’t impose an artificial loop—we describe the natural recursive relationship and trust it to unfold.
This requires the same faith Stoics required: trust that nature (or in this case, the call stack) will handle the mechanics. Your responsibility is defining the relationship, not controlling execution.
Tail recursion optimization makes this practical, just as Stoic practice makes philosophical acceptance practical.
10. Lazy Evaluation: Patience and Present Focus
The Stoics taught focusing on the present moment. Marcus Aurelius wrote: “Confine yourself to the present.” Don’t borrow trouble from the future or linger in past regrets.
Lazy evaluation implements this temporal focus in code. Computations don’t happen until their results are needed—no premature effort, no wasted calculation.
let infiniteNumbers = [1..] let firstTen = take 10 infiniteNumbers
This code defines an infinite list but doesn’t attempt to compute it. Only when you request specific values does evaluation occur. It’s perfectly content existing as pure potential until the present moment demands actuality.
This mirrors Stoic practice: don’t exhaust yourself preparing for every possible future. Focus on what the present requires. When future moments arrive, they’ll carry their own requirements—deal with them then.
Lazy evaluation prevents the programmer equivalent of anxiety: premature optimization, over-eager caching, and computing values “just in case.” Do what’s needed now. Trust that future needs will be met when they arise.
11. Higher-Order Functions: Universal Patterns
Stoics believed in logos—the rational principle governing the universe. Specific events differ, but universal patterns repeat. Marcus Aurelius observed: “Nothing happens to any man that he is not formed by nature to bear.”
Higher-order functions recognize this same principle—universal patterns that apply across specific instances.
Instead of writing:
function doubleNumbers(numbers) {
const results = [];
for (let n of numbers) {
results.push(n * 2);
}
return results;
}
function uppercaseNames(names) {
const results = [];
for (let name of names) {
results.push(name.toUpperCase());
}
return results;
}
You recognize the universal pattern:
const map = (fn, items) => items.map(fn); const doubleNumbers = numbers => map(n => n * 2, numbers); const uppercaseNames = names => map(name => name.toUpperCase(), names);
The pattern—transformation of a collection—appears once. Specific instances specialize it. This is exactly how Stoics viewed virtue: universal principles applied to specific circumstances.
Functions like map, filter, reduce, and compose are the programming equivalent of Stoic maxims—universal truths that guide specific actions.
12. Freedom Through Constraint: The Paradox of Discipline
The Stoic paradox: accepting limitations brings freedom. Epictetus said: “Freedom is secured not by the fulfilling of one’s desires, but by the removal of desire.”
Functional programming manifests this paradox. Constraints that seem limiting actually liberate:
Immutability seems restrictive—you can’t change anything! But it liberates you from defensive coding, threading bugs, and unexpected state changes.
Pure functions seem limiting—no side effects! But they liberate you from debugging mysterious interactions and enable fearless refactoring.
Explicit data flow seems verbose—you must pass everything explicitly! But it liberates you from hidden dependencies and makes systems comprehensible.
This mirrors exactly how Stoic discipline works. Accepting that you cannot control external events seems limiting—until you realize it frees you from anxiety about those events. Focusing on virtue seems restrictive—until you realize it frees you from dependence on circumstances.
The Elm programming language embraces this philosophy completely. Its constraints—no null values, no runtime exceptions, no side effects—seem draconian. Yet Elm developers report unusual clarity and confidence. The constraints don’t limit—they channel creativity productively.
13. Error Handling as Acceptance of Fate
Stoics prepared for adversity. They didn’t assume everything would go well—they built mental resilience to handle whatever occurred.
Functional error handling embodies this preparation. Instead of exceptions that disrupt control flow (like unexpected emotional reactions disrupting Stoic equanimity), functional programming uses types that explicitly represent success or failure:
// Imperative: hope for the best
function divideNumbers(a, b) {
return a / b; // Division by zero produces Infinity
}
// Functional: prepare for outcomes
function divideNumbers(a, b) {
if (b === 0) return { success: false, error: 'Division by zero' };
return { success: true, value: a / b };
}
The functional version accepts that operations might fail. It doesn’t pretend problems won’t occur—it prepares explicitly for multiple outcomes.
Rust’s Result type makes this philosophical stance mandatory. Every potentially failing operation must acknowledge the possibility of failure. You cannot pretend things will work out—you must demonstrate preparation.
This is premeditatio malorum in code: visualize potential failures, prepare for them, and maintain composure when they occur.
14. Time and State: The Flowing River
Heraclitus, a pre-Socratic who influenced Stoicism, said: “No man ever steps in the same river twice, for it’s not the same river and he’s not the same man.”
State machines in functional programming recognize this temporal reality. You don’t have a “user object” that changes—you have a sequence of immutable user states flowing through time:
const userV1 = { name: 'Alice', email: 'alice@old.com' };
const userV2 = updateEmail(userV1, 'alice@new.com');
const userV3 = verifyEmail(userV2);
Each version is distinct and permanent. You cannot revisit userV1 and change it—that moment has passed. You can only move forward, creating new moments from old ones.
This aligns perfectly with Stoic temporal philosophy. The past is gone and unchangeable. The future is uncertain and beyond control. Only the present moment—the transition from one state to the next—is yours.
15. Testing as Philosophical Practice
Stoics practiced examining their thoughts and behaviors. Seneca recommended daily review: “When the light has been removed and my wife has fallen silent, I examine my entire day and go back over what I’ve done and said, hiding nothing from myself, passing nothing by.”
Property-based testing implements this philosophical examination. Instead of testing specific cases, you test universal properties that should hold:
// Property: reversing twice returns original
property('reverse is its own inverse',
forAll(array(), arr =>
deepEqual(reverse(reverse(arr)), arr)
)
);
This tests your philosophical understanding—the essence of reversal—rather than specific instances. You’re examining whether your code aligns with logical principles, not just whether it works in specific scenarios.
This mirrors Stoic self-examination: do your actions align with virtue? Do your beliefs align with reason? Are you living according to nature?
16. The Practical Benefits of Philosophical Alignment
This isn’t merely theoretical parallelism. Functional programming’s philosophical foundations produce concrete benefits:
Reasoning about code becomes easier when you accept immutability. You don’t track what might have changed—you follow transformations.
Concurrency becomes manageable when nothing mutates. Race conditions disappear when there’s no race—data doesn’t change.
Testing becomes straightforward when functions are pure. Same inputs always produce same outputs.
Debugging becomes tractable when referential transparency holds. You can reason locally rather than considering entire system state.
Refactoring becomes safe when side effects are explicit. You know exactly what might be affected.
These technical benefits emerge from philosophical alignment—accepting reality’s nature rather than fighting it.
17. Living the Stoic-Functional Life
Both traditions offer more than techniques—they offer ways of being. The Stoic programmer:
- Accepts what cannot be changed (immutable data) while focusing energy on what can be created (new values).
- Controls their response (pure functions) rather than attempting to control everything around them (side effects).
- Focuses on intentions (declarative code) rather than getting lost in mechanics (imperative details).
- Prepares for uncertainty (Maybe/Result types) rather than pretending everything will work out.
- Recognizes universal patterns (higher-order functions) while respecting specific circumstances.
- Finds freedom through discipline (constraints like immutability) rather than through unlimited options.
This produces both better software and better developers. The psychological benefits mirror the technical ones: less anxiety about state changes, more confidence in correctness, greater clarity in reasoning, and reduced cognitive load.
18. Beyond the Code: Philosophy as Practice
Marcus Aurelius didn’t write Meditations for publication—he wrote it as personal philosophical practice. The Stoics understood that philosophy isn’t theoretical knowledge but practical wisdom developed through daily application.
Functional programming offers the same opportunity. Every time you choose immutability over mutation, you practice acceptance. Every time you write a pure function, you practice controlling your response. Every time you compose functions, you practice seeing universal patterns.
The code becomes a practice ground for philosophical discipline. Like martial arts or meditation, the technical practice cultivates mental habits that extend beyond the practice itself.
You begin to see life itself functionally: accepting what is, creating what should be, focusing on your response rather than external circumstances, and finding freedom through chosen constraints.
19. Conclusion
The ancient Stoics and modern functional programmers discovered the same truth through different paths: freedom emerges from accepting what cannot be changed while focusing intensely on what lies within your control. Immutability mirrors Stoic acceptance—the past exists as fixed reality. Pure functions embody virtue—controlling your response rather than external events. Declarative thinking reflects proper philosophy—focusing on intentions over mechanisms. When Marcus Aurelius wrote about distinguishing what we control from what we don’t, and when functional programmers separate pure logic from side effects, they’re teaching the same wisdom: profound power lies not in changing the world, but in how we create our response to it.





No, the actual stoic programming language isn’t functional, but rather concatenative. I’m speaking of the Forth-like STOIC programming language created in the 1970s by Jonathan Sachs at MIT (Sachs would later go on to code Lotus 1-2-3, a spreadsheet that was very popular before Excel). Stoic still exists today as STOICAL (STack Oriented Interactive Compiler Adapted to Linux)
Hello Jonathan, That’s a genuinely fascinating point, and you’re absolutely right to flag it! The article uses ‘Stoic’ purely as a philosophical metaphor, but you’ve highlighted something worth clarifying: there is an actual programming language called STOIC — Stack Oriented Interactive Compiler, created by Jonathan Sachs at MIT in the early 1970s. As you note, STOIC is concatenative and Forth-like, not functional in the modern sense. The parallel is ironic in a fun way though: STOIC the language is stack-based and operates by transforming values through composition of operations — which does share *some* spirit with functional programming’s emphasis on… Read more »