पाठ 21 / 25
Functional Core, Imperative Shell
Effects at the edges.
An architecture, not just a technique
The functional core, imperative shell pattern (popularised by Gary Bernhardt) puts decisions in a core of pure functions over immutable data and keeps all I/O in a thin shell. The shell gathers inputs (HTTP request, database rows, current time), calls the core, and executes the outputs (writes, emails, responses). The core can be unit-tested exhaustively and quickly; the shell has little logic and is covered by a few integration tests. Similar ideas appear as hexagonal architecture (ports and adapters), the Elm architecture and Redux (pure reducers, effects in middleware).
Core returns decisions, shell executes them
Effects are described as data.
// ---- functional core (pure) ----
type Account = { id: string; balance: number; overdraftLimit: number };
type Command =
| { kind: 'debit'; accountId: string; amount: number }
| { kind: 'notify'; accountId: string; message: string };
function withdraw(acc: Account, amount: number, now: Date): Command[] {
if (amount <= 0) return [];
if (acc.balance - amount < -acc.overdraftLimit) {
return [{ kind: 'notify', accountId: acc.id, message: `Declined at ${now.toISOString()}` }];
}
return [{ kind: 'debit', accountId: acc.id, amount }];
}
// ---- imperative shell (effects) ----
async function handleWithdraw(repo: AccountRepo, notifier: Notifier, id: string, amount: number) {
const acc = await repo.load(id);
for (const cmd of withdraw(acc, amount, new Date())) {
if (cmd.kind === 'debit') await repo.debit(cmd.accountId, cmd.amount);
else await notifier.send(cmd.accountId, cmd.message);
}
}
interface AccountRepo { load(id: string): Promise<Account>; debit(id: string, amt: number): Promise<void> }
interface Notifier { send(id: string, msg: string): Promise<void> }A judge and a bailiff
The judge (core) only decides based on the evidence presented; the bailiff (shell) gathers people and carries out the ruling. You can review a ruling without re-running the arrest.
त्वरित जाँच: In a functional core / imperative shell design, where does the database call belong?
- Inside every core function
- In the shell, which passes loaded data into the pure core
- In the type definitions
- Nowhere; databases are not allowed in FP
Answer
In the shell, which passes loaded data into the pure core — The core decides; the shell performs.