Hardly Readable - Imperative and Declarative
“Declarative says what, imperative says how.” Often heard - and in need of explanation at the very least.
Is SQL declarative? Statements begin with SELECT, INSERT, UPDATE, DELETE. For anyone who knows their grammar: those are all imperatives. And accordingly, a statement reads more like a description of HOW one result is built out of another.
Is Java imperative? What if I claimed that in Java, too, you often describe the WHAT? The essential property of object-oriented languages is, after all, that concrete implementations - the HOW - are hidden.
Both words are centuries older than programming, and both are Latin: declarare is to assert, imperare is to command. That makes the question worth asking one about the line in front of us rather than about the language it is written in - does it read like a sequence of commands, or like a set of relational statements? This part of the Hardly Readable series starts from that question and follows it to what the two styles cost the reader.
The formula is fine - the examples are not
“Declarative says what, imperative says how” is repeated so often that it feels like a definition, and there is nothing wrong with it. It names a real difference: how much of the how an expression leaves unsaid. What goes wrong is the next step, where the formula is handed to a language as a label and the code is expected to inherit it.
SQL is the declarative language everyone points to, and yet every statement in it opens with a command: select these columns, insert that row, delete those. Grammatically there is nothing declarative about it at all - subjectless verbs in the imperative mood, addressed to an implied listener, expecting the listener to act. Nor does the body of a query rescue it:
SELECT title
FROM book
JOIN loan ON loan.book_id = book.id
WHERE loan.member_id = 42
Nothing in that text asserts a relation; it describes how to build one. Take book, join loan onto it over the book id, keep the rows where the member is 42, project the title - a recipe, with an order, not far from the cursor loop it replaces (open, fetch, test, append, advance, close). Why SQL carries the declarative label all the same is a fair question with a good answer, and it is taken up at the end of this article, once the distinction that does the work is in place.
Java is the imperative language, and yet:
var loan = member.borrow(book);
That line is a claim. It relates a member, a book and a loan, it is true when you read it and it stays true afterwards - and nothing in the language forces anything else. A Java program built from single assignments and returned values reads as a set of relations from beginning to end.
Neither label survives its own examples, because they are labels on the wrong thing. Declarative and imperative are a matter of style - a property of the line in front of you, not of the language it is written in.
Two words borrowed from grammar
Neither word was coined for programming. Both were grammatical terms for centuries before there was code, and both come straight out of Latin.
Imperative comes from imperare, “to command” - the same root as imperator, the one who gives the orders. Latin grammarians used the term for the verb form that commands, and three of its properties carry over to code unchanged: a command is addressed to someone, it carries no subject of its own, and it is uttered in order to change something. That expected change of state is the reason for saying it at all.
Declarative comes from declarare, “to make plain” - de- intensifying clarus, “clear”. A declarative sentence asserts that something is the case. It tells nothing to behave in any way; it puts things in relation to one another and claims that the relation holds.
That gives the sharpest test between the two, and it separates them completely: a declarative sentence is true or false; a command is neither. An assertion has to fit the world - if the world is otherwise, the sentence is wrong. A command works the other way round: the world has to be changed to fit the words, and if the world is otherwise, that is the point of uttering it rather than a defect in it. (Linguists call this difference the direction of fit.)
Carried over to code:
- An imperative line is a command with an addressee and an expected change of state.
exp *= atells the machine to make something other than it is. It is not true or false - before it runs it is not yet the case, after it runs it is no longer being asked for. - A declarative line is a claim. It relates things to one another -
sumtoaaandbb, a loan to a book and a member - and says the relation holds. It prescribes no behaviour, and it can be checked: it is true, and it stays true.
Most code is a mixture of the two - a SQL statement is a command wrapped around a build recipe, a Java method is a sequence of commands with claims scattered through it - and the ratio is what we are really judging when we call code declarative or imperative.
Everything below follows from that one asymmetry. A claim can be read and kept. A command has to be carried out - by the machine, and by the reader in their head - before anyone knows what is true afterwards. That is where the consequences start, and they are not aesthetic: “hard to read” has a measurable substrate, working memory (see Code and Cognition), and the two styles spend it differently.
The same computation, twice
Take one small calculation - the length of a vector, sqrt(a*a + b*b) - written two ways.
Declarative:
var aa = a * a;
var bb = b * b;
var sum = aa + bb;
var result = sqrt(sum);
Imperative:
var exp = a;
exp *= a;
exp += b * b;
exp = pow(exp, 0.5);
Both produce the same number. They do not cost the same to read, and the reason is not length or cleverness - it is what each line asks of the reader’s memory.
A declarative line states a relationship
var aa = a * a;
var bb = b * b;
var sum = aa + bb;
var result = sqrt(sum);
Every line here is a standing fact - a relationship that is true when you read it and stays true afterwards. aa is a * a, and it never becomes anything else. sum is aa + bb. Each name means one thing for the whole life of the snippet, so once you have chunked aa you never have to revisit it; you just use it. The lines combine the way clauses combine into a sentence: four relationships, assembled into one whole. Four chunks, four steps, nothing taken back.
You read this code. Nothing has to be run in your head to know what is true.
An imperative line changes state
var exp = a;
exp *= a;
exp += b * b;
exp = pow(exp, 0.5);
There is not one standing fact in these four lines. exp is not a relationship; it is a slot whose contents change on every line. It starts as a, then it is a*a, then a*a + b*b, then the square root of that. To know what exp means on the third line you must already have executed the first two in your head. The name is stable but its meaning is not - and that is the expensive part.
Each line does two things to working memory at once: it establishes a new value for exp, and it invalidates the old one. The chunk you built on the previous line is now wrong; it has to be discarded and rebuilt. The talk this series grew out of counts it exactly: the same calculation is four steps when written declaratively, and seven steps with three invalidations when written imperatively. The invalidations are the tax. You do not read this code; you simulate it.
Invalidation is the cost
A declarative line adds a chunk that stays valid, so understanding accumulates monotonically. An imperative line replaces one: the reader tracks the new state and has to remember that the previous one is gone. Reusing a single mutable name, as exp does, is the worst case - the same name means different things at different lines, and only replaying the sequence tells you which.
The goal is not to fit the program into working memory. Working memory holds about four chunks, and no realistic knowledge base - a class, a module, a system - ever fitted into four. What matters is whether the knowledge base is represented so that any part of it is immediately processable when it is looked up. In declarative code the lookup is the answer: go to the line that establishes sum, and sum is aa + bb. In imperative code the true state is nowhere in the text - to learn what exp is at some point you have to start from a point you do know and re-simulate forward. The knowledge base is not stored, it is recomputed on every access.
Abstraction does not level this out, it widens it. var loan = member.borrow(book); adds one chunk - there is a loan of this book to this member - a claim at exactly the level you chose to read at, and nothing below the line takes back what you knew. member.borrow(book); also changes state - the member’s loan count, the availability of the copy, perhaps a waiting list - and says nothing about which. So the imperative line charges twice: first for the investigation of what has to be invalidated and what has just come into existence, then for the invalidation itself. That first cost is invisible in a four-line snippet and dominant in a large system, where every command is a question about the state of the world with no answer written next to it.
A fun fact on the side, hard to unsee once noticed: comments in imperative code are overwhelmingly translations of commands into declarative meaning:
i++; // i now points at the next unread entry
flush(); // the buffer is empty
Nobody documents a declarative line that way; there is nothing to translate. The people writing those comments had worked it out long ago: commands have to be simulated, claims can be read - so they supplied the claim by hand, because the code would not.
That is the readability gap, stated in the currency of this series. Declarative code spends chunks on relationships, which compose, persist and stay at the level they were written at. Imperative code spends them on states, which expire, have to be tracked down, and have to be re-derived. The first is reading; the second is running a simulation by hand.
The same split lives inside a single call
The divide is not only between styles of program; it appears inside one line. A companion piece, Source Code Is Language, shows that a method call can be read two ways - as a statement about the world or as a command to an object. Those are exactly the declarative and the imperative readings.
Read as a statement, member.borrow(book) declares a relationship: the member borrows the book, a fact you can take off the line. Read as a command - “Member, borrow the book!” - it performs a step, and what is true afterwards is left to the method’s contract, to be simulated rather than read.
But there is a finer point, and it is the one most easily missed. Reading the statement creates the relationship - in the reader’s memory. When you read member.borrow(book) or items.sort(byPrice), you form the relation - the member has the book, the items are in price order - whether or not the call returns anything. That cognitive relation is free; it comes with understanding the line.
What it does not do is give the program anything to hold. A relation in your head is not a value in the code. For the code to refer to the relationship again - to pass it on, query it, combine it - it has to be stored in a result, bound to a name. A void command sets the relation in the world and in your mind, but leaves nothing to grab:
member.borrow(book); // you understand the loan; the code cannot refer to it
Designed to return the relationship, the same operation makes it addressable:
var loan = member.borrow(book); // now the loan is a result the program can use
The loan is a fact in either case; the binding is what turns a relationship you understand into one the code can use.
sort shows it most clearly
The split is sharpest where the whole point of the call is to produce something you go on to use:
items.sort(byPrice);
The relationship - the items are now ordered by price - holds afterwards, and you grasp it as you read. Yet nothing holds it for the program: the only handle is items itself, mutated in place, its old unsorted meaning silently overwritten - the invalidation from the imperative example, now inside a single call. Store the outcome instead, and the ordering becomes a value the code can name, while the original list stays itself:
var sorted = items.stream().sorted(byPrice).toList();
sorted is the relationship made referrable. The reader understood “the items in price order” either way; only here can the rest of the program refer to it.
One caveat, so the analogy is not over-read: byPrice is not the relationship. It is the criterion the sort runs by - a means, not a participant (grammatically an adverbial of means, an ablative, not the object of the verb; the thing acted on is items alone). The relationship that matters is the post-state - the items in order - which is exactly what the void form fails to store.
So why is SQL called declarative?
Back to the question left open at the beginning. If a SQL statement is a command with a build recipe in its body, why is it the language every textbook reaches for when it needs a declarative example? Three answers are usually given, and only the last one survives.
“SQL states the result; the engine works out the plan.” True - and it does not distinguish anything. A call in an imperative language does exactly the same: member.borrow(book) names an outcome and leaves the execution to whatever is below the line, where an implementation, a compiler and a JIT are free to inline, reorder and rewrite it beyond recognition. Naming an outcome and delegating the execution is what every abstraction does. If that were the criterion, every language with methods would be declarative.
“In an imperative language, every detail is part of the implementation.” This one holds, and it does distinguish. A high-level command in Java delegates to lower-level commands, those to lower ones again, and the whole chain is yours: readable, steppable, written in the same language, bottoming out in code you could have written. A SQL statement is handed to an engine and interpreted. The plan is not part of your program at all, and looking into it means EXPLAIN output and a vendor manual rather than a debugger. That is a real difference - but it is a difference about who owns the implementation, not about how the code reads. And in my opinion it does not make SQL declarative: SQL statements often feel more imperative than statements in the so-called imperative languages, and the fact that we could step into a Java call does not make the facade of that call any less of a statement. Opacity is not assertion.
“SQL is equivalent to Datalog, and Datalog is declarative.” This is the argument that stays. A SQL query denotes an expression of the tuple relational calculus, and that calculus is equivalent in expressive power to relational algebra, to the domain relational calculus, and to (non-recursive) Datalog - Codd’s theorem. The same query can be moved between them without gain or loss. And Datalog is declarative in exactly the sense this article means. A rule is a claim:
borrowed(Title, Member) :- loan(Book, Member), book(Book, Title).
No addressee, no order, nothing taken back - it states that a relation holds: whenever there is a loan of a book to a member and that book has a title, the pair is in borrowed. And the SQL that says the same thing is the view the earlier query was built out of:
CREATE VIEW borrowed AS
SELECT book.title, loan.member_id
FROM book
JOIN loan ON loan.book_id = book.id
The two are the same statement in different notation, clause for clause. The shared variable Book in the rule is the join condition in the view; the two body goals loan(...) and book(...) are the two tables in the FROM/JOIN; the head arguments Title, Member are the SELECT list. Asking for one member turns the rule into a query, ?- borrowed(Title, 42)., and the view into SELECT title FROM borrowed WHERE member_id = 42 - which is the query this article opened with. Nothing is added on either side, and nothing is lost.
The difference is only in how the two texts read. The rule reads as a claim; the view reads as an instruction to build one. Because SQL is one notation for something that has a fully declarative twin, it inherits the twin’s label.
That inheritance is a property of the semantics, though, and not of the text in front of the reader - which is why the label helps so little when you are actually reading a query, and why the question this article started from is asked per line and not per language.
What a paradigm label claims
The same reasoning explains the labels on the general-purpose languages, and why they are weaker than they look.
Java’s paradigm is far more powerful than Datalog’s. It has state, sequence and unrestricted effects, and it can express things no relational calculus can. That power has a price: a language that can do anything must be able to command, so it cannot be declarative all the way down, and the label follows what the language can do rather than what you do with it. Java is filed under imperative - and var loan = member.borrow(book); is still a statement, and a program built out of single assignments and returned values still reads as a set of relations.
Haskell and ML are the mirror image. A functional language makes imperative writing hard - there is no assignment to reach for, and effects do not compose silently - which is why declarative style is the default there instead of a discipline. But a general-purpose language cannot do without effects, so the imperative part comes back through a backdoor: IO and ST, mutable references, do notation that is a sequence of commands in everything but spelling. It is cleverly boxed - the type system keeps it from leaking into the rest of the program - but inside the box they are commands, and the reader reads them as commands.
So the honest version of the difference between Java and Haskell is not declarative versus imperative. Java claims to be imperative, and declarative writing is possible in it. Haskell claims to be functional and declarative, and imperative parts are necessary in it. What is left over in both is the question the reader actually faces on every line: is this a claim I can take and move on, or a command I have to carry out in my head?
The takeaway
Prefer the form that states relationships over the one that walks through states: single-assignment over reassignment, a value returned over a slot mutated, a named result over a name you keep overwriting. And where something genuinely has to change, let everything around the change stay a claim. That keeps each relationship both readable and referrable - present in the reader’s mind as a standing fact, and present in the program as a value it can use. The test is the one this whole series turns on, and it is asked of the code and not of the language it is written in - can the reader take the line as a standing fact and move on, or must they run the program in their head to learn what is true? Declarative code lets them read. Imperative code makes them simulate, and every invalidation along the way is a charge on a budget they do not have.
More
For related discussion and background, see:
- Code and Cognition - why “hard to read” has a measurable cognitive cost, and why simulating state in your head is exactly what working memory is worst at
- Source Code Is Language - the companion piece, where the same declarative/imperative split appears inside a single call as the difference between reading it as a statement and as a command
- Declarative Programming - the usual what, not how framing, and the list of languages called declarative that the framing does not quite fit
- Codd’s Theorem - the equivalence of relational algebra and the relational calculi that SQL’s reputation as a declarative language actually rests on
- Datalog - the declarative twin: rules that state relations, with no addressee and no order
- Imperative Mood - the grammatical form the word is borrowed from: addressed to someone, subjectless, and neither true nor false
- Direction of Fit - the linguistic statement of the split: an assertion has to fit the world, a command asks the world to fit it
- Referential Transparency - the formal name for the property this article reads for: an expression that can be replaced by its value without changing meaning, because it states a relationship that stays true