Programming Paradigms

A programming paradigm is a style — a way of thinking about and structuring computation. It answers the question "what is a program made of?" One paradigm says a program is a sequence of commands that change memory; another says it is a collection of objects sending each other messages; a third says it is an expression to be evaluated; a fourth says it is a set of facts and rules to search. Same machine, radically different mental models.

Paradigms are lenses, not languages. Once you can recognise the lens, you read unfamiliar code faster, you pick the right tool for a problem, and you understand why the same idea looks so different in Python, Haskell, and Prolog. This page introduces the four great families and the single axis that organises them.

The one axis: HOW vs WHAT

The deepest division runs between imperative and declarative styles. Imperative code tells the machine how to reach the answer, step by step — "make a running total, walk the list, add each number." Declarative code states what the answer is and leaves the "how" to the language — "the answer is the sum of the list." The four families spread out along that line: imperative and object-oriented lean toward how; functional and logic lean toward what.

Four families

The same task, two ways — run it

Nothing makes paradigms concrete like solving one problem in two styles. The task: take a list of numbers and compute the sum of their squares. The imperative version keeps a mutable accumulator and mutates it in a loop. The functional version describes the answer as a pipeline — map each number to its square, then reduce to a total — with no mutation at all. Both give the same number. Press Run:

const nums = [1, 2, 3, 4, 5]; // IMPERATIVE: spell out HOW. A mutable accumulator, mutated step by step. function sumSquaresImperative(xs: number[]): number { let total = 0; // mutable state for (let i = 0; i < xs.length; i++) { total = total + xs[i] * xs[i]; // mutate the accumulator } return total; } // FUNCTIONAL: describe WHAT. Pure functions, no mutation, no loop. function sumSquaresFunctional(xs: number[]): number { return xs .map((x) => x * x) // each number -> its square .reduce((a, b) => a + b, 0); // fold the squares into a total } console.log("imperative:", sumSquaresImperative(nums)); console.log("functional:", sumSquaresFunctional(nums)); console.log("same answer? ", sumSquaresImperative(nums) === sumSquaresFunctional(nums));

Look at the difference in what the code talks about. The imperative version talks about an index i, a running total, and the act of stepping — machinery. The functional version talks about squaring and summing — the problem itself. Neither is "better" everywhere; they are different lenses on the same computation.

And the other two lenses

The same "add up a collection" idea looks different again in the object-oriented and logic families. In OOP you'd wrap the data and its behaviour together, so the collection knows how to total itself:

class NumberList { constructor(private readonly items: number[]) {} // behaviour bundled WITH the state it acts on sumOfSquares(): number { return this.items.reduce((a, x) => a + x * x, 0); } } const list = new NumberList([1, 2, 3, 4, 5]); list.sumOfSquares(); // the object computes it — you send a message

In the logic family you don't describe a procedure at all. You state relationships as facts and rules, and pose a query; the engine searches for the answer. Here is the idea in a Prolog-flavoured sketch — sum_squares is defined, not executed by you:

% base case: the sum of squares of the empty list is 0. sum_squares([], 0). % rule: square the head, recurse on the tail, add them. sum_squares([H | T], Total) :- sum_squares(T, Rest), Total is Rest + H * H. % a QUERY — you ask, the machine searches for a value of X: ?- sum_squares([1, 2, 3, 4, 5], X). X = 55.

Notice there is no loop and no accumulator you manage — you declared what it means to be the sum of squares, and the resolution engine found X = 55 on its own. That is the essence of the declarative end of the spectrum.

Logic programming grew out of formal logic and automated theorem-proving in the early 1970s; Prolog (1972) was the breakthrough. It shines wherever a problem is naturally a web of rules and relationships: parsing natural language, expert systems, scheduling and constraint puzzles, and databases (SQL is itself largely declarative — you say which rows you want, not how to fetch them). Datalog, a Prolog relative, powers modern program-analysis and policy engines. The style feels alien at first because you stop writing steps — but for the right problem, declaring the rules and letting the machine search is astonishingly compact.

Paradigms are lenses — most languages are several at once

Here is the crucial point that beginners miss: a paradigm is a way of thinking, and a single language can support several. JavaScript and Python are imperative and object-oriented and functional — you choose the lens per problem. Scala and F# blend functional and object-oriented deliberately. Even C++ spans imperative, OOP, and (with templates and the STL) a good deal of functional style. "Multi-paradigm" is the norm, not the exception.