Reading, Meaning and Sense
Reading a line of code feels like one act. It is at least three. We form a meaning from our own context, we compare it - if we bother - with the meaning the author had, and we form an opinion about whether what it describes makes any sense. The first two are reading. The third is judgement, and it arrives uninvited, mixed into the same moment, wearing the same clothes.
This part of the Hardly Readable series is about keeping them apart, using an example that failed at two of the three steps while looking flawless at the first: a line so easy to read that it took me a while to notice it was describing the wrong thing, and then the wrong design.
Three things we do when we read a line
Take a single line of code and watch what happens.
What do I read? A meaning assembled from my own context: which word is the subject, which is the predicate, which is the object, and what those roles mean in the world I bring to the line. This is what Source Code Is Language was about. When the roles sit where my native grammar expects them, I recognise the sentence instead of decoding it, and it costs me almost nothing.
What was meant? The meaning the author had when writing it. Ideally the same one. If it is not, we do not share an understanding of the code, and that is a readability problem - not a design problem, not a domain problem. The line failed to transport what it was supposed to transport.
Does it make sense? Whether the relation described is a good one: whether the responsibility sits with the right object, whether the types tell the truth about the domain. This is not reading at all. It draws on knowledge of the domain that the line does not contain and cannot supply.
The first two are readability. The third is not - but it runs anyway, at the same time, and we rarely notice the seam.
There is an asymmetry between the first two that matters more than it looks. The first question I can answer alone: I know whether the line cost me effort. The second I cannot. Fluency feels exactly the same whether the model I built is the intended one or not. A line I cannot read at least announces that I need to go and look something up. A line I can read wrongly announces nothing - I stop reading precisely when I am most mistaken.
What I read
The example is the one the earlier article originally used:
alice.transfer(amount).to(bob);
It reads beautifully. alice is the subject, transfer the predicate, amount the object, and bob follows the preposition to exactly where English puts a recipient. The call has the same word order as the sentence Alice transfers the amount to Bob. Nobody has to remember whether the first position means source, destination, or sum. The roles are marked, not positional.
So what do I read? Alice hands money to Bob. Two people, one payment, no effort. By the first measure this line is not merely readable, it is unusually readable - it borrows a story I already know.
What was meant
Here is the rest of the context:
Account alice = ...;
Account bob = ...;
alice and bob are accounts. What the author meant was that money moves from one account to another. What I read was that a person gives money to a person.
These are not the same statement. People initiate payments; money is posted between accounts. Alice transfers money to Bob is acceptable shorthand in conversation and useless as a description of what a banking system does - Alice may hold three accounts, the transfer may be initiated by neither party, and the accounts are the only things the code will ever hold.
The gap between the two readings is the defect, and it is a readability defect. The design has not been discussed yet. The line simply delivered a different meaning than the one it was given, and it delivered it so smoothly that nothing prompted me to check. The names supplied a model - people - and the model was wrong.
Correct the names and the two readings coincide:
accountA.transfer(amount).to(accountB);
Now the line says that one account transfers money to another account, and that is what was meant. Both readability questions are answered. The line is readable and it is understood as intended.
Does it make sense
The line still reads perfectly. Nothing about it is unclear. And the design is still wrong.
accountA now holds a reference to accountB and drives a process that spans both of them. In a bank, that is the coupling we least want. An account should protect what is its own: its currency, its overdraft limit, whether it is frozen. It has no business knowing that other accounts exist, and a design that hands it one invites every future operation involving two accounts to be hung off the same place.
The responsibility does not fit there either. A transfer is a debit and a credit, and neither half may exist alone. If the debit succeeds and the credit fails, money was destroyed; if the credit succeeds and the debit fails, money was created. No account can guarantee that pair. The first must not be responsible for the second’s balance, and the second cannot know whether the first was debited. Putting transfer on one participant makes it answerable for an outcome it does not control.
A complex process is better owned by something that exists for it. The obvious move is to give the process its own object:
Transfer.of(amount)
.from(accountA)
.to(accountB)
.execute();
This is better. The transfer owns both postings, and with them its identity, its status, and its failure reason. Neither account is answerable for the other any more.
But now ask the question the first version could not answer either. Tomorrow a customer reports that money left one account and never arrived. Where do I find that transfer?
Nothing in this code holds it. Transfer.of(...) manufactures it, execute() consumes it, and the statement ends. The transfer exists for the length of one expression. So the failed one is somewhere else: a database table, or a repository the constructor quietly reached for, or a static field. The code offers no object to ask.
The grammar shows this before the design does. Transfer.of(...) is a call on a class, and a class is not an object in the running program. The sentence has no subject that existed before the line or survives it. It is the procedural form the earlier article warned about - transfer(accountA, amount, accountB) - with the roles marked properly this time. Better labelled, equally unattached.
The chain itself is not the problem. Read as English it is a perfectly good noun phrase: a transfer of the amount from accountA to accountB, with every preposition where English puts it. The problem is that the noun phrase is handed to nobody. A statement whose subject was manufactured on the spot, and whose result is given to no one, has no connection to the rest of the program. Statements like that always leave their results off-page, and off-page means a global or the database.
So give the sentence a subject that was already there:
Transfer transfer = Transfer.of(amount).from(accountA).to(accountB);
ledger.post(transfer);
The ledger posts a transfer of the amount from accountA to accountB. Subject, predicate, object - and the noun phrase now sits in the object slot, which is where a noun phrase belongs.
The subject exists before the statement and after it, and that answers the question. Where do I find failed transfers? In the ledger. The receiver of the verb is the home of the result. Posting a balanced pair of entries is what a ledger is for, so the two-sided rule has an owner that exists for it, and no account has been handed a reference to another - the transfer names both accounts, but a transfer being about two accounts is a different thing from an account knowing one.
Three versions, one question:
accountA.transfer(amount).to(accountB)- a real subject, but the wrong home. A transfer has to be reconstructed from the accounts that took part in it.Transfer.of(amount)...execute()- the right concept, no home at all. Transfers are found in the database, or through a global.ledger.post(transfer)- the right concept, in a place the code can name. Transfers are found by asking the ledger.
A fourth variant is worth weighing. Keep the original sentence, but let it build the transfer instead of performing it, and leave the execution to the ledger:
Transfer transfer = accountA.transfer(amount).to(accountB);
ledger.post(transfer);
This states the relation more clearly than Transfer.of(...) does. The roles are carried by the grammar rather than assembled out of a class-level call: accountA is the source because it stands in front of the verb, accountB is the destination because it follows to, and the phrase begins with an object that actually exists rather than with a type name. Nothing is executed here either, so no account touches another’s balance - accountA only names the other.
What it risks is the gap this article is about. transfer is a verb, and on that line it does not act. Read as a sentence it still says accountA transfers the amount to accountB, a completed action, where what was meant is a description of one. The Transfer transfer = on the left corrects that, and var would take the correction away again. Whether that is an economy or a trap depends on how reliably a reader takes in the assignment before reading the sentence - which is a judgement about readers, not a rule about design.
And notice what none of this cost. Nothing in accountA.transfer(amount).to(accountB) was ever unclear. The objection to it came from knowing how banks work, not from reading the line, and no amount of further reading would have produced it. But once the objection is made, the better design reads just as easily as the line it replaces. Readability and maintainability are not in tension here. They are different questions, and answering one tells us nothing about the other.
Reading is not approval
The cost of running the third step inside the first two is that fluency gets mistaken for correctness. A line that reads well passes review; we say readable and we mean fine. The smoother it reads, the less it invites the question of whether what it describes should be true at all. This is the failure mode of an example like alice.transfer(amount).to(bob): it is so pleasant to read that it takes an effort of will to ask what it is claiming.
The reverse happens too. A design we dislike gets called unreadable when what we mean is that it is wrong. Then the discussion is about naming and formatting, and the actual objection never gets stated.
Separating the steps makes both cases sayable. This line reads perfectly, I understood it as you meant it, and the responsibility is in the wrong place - three verdicts, three different remedies.
Why Alice and Bob got in
I am used to Alice-and-Bob examples and thought they were a good idea here. They were not, and the reason is worth stating, because it is what made the second step fail.
Alice and Bob come from protocol and cryptography literature. Their whole value there is that they are people with intent: Alice wants to send a message, Bob wants to receive it, Eve wants to intercept it. A protocol only makes sense as a story about parties with motives, and named characters carry those motives for free. In that genre the names are excellent, which is exactly why they have survived for decades.
That strength is the defect here. The names did not merely fail to say account - they actively supplied a person-model, because that is the genre they arrive with. The declarations two lines above stood no chance; a reader recognising Alice and Bob recognises people before reading any type. What I read was decided by the genre, not by the code.
There is a second mismatch underneath. Alice and Bob describe one run of a protocol - a concrete scenario, a test, an exploit. Source code describes all runs. A production transfer does not contain Alice and Bob; it contains whichever two accounts were supplied to it, and its names will be generic:
accountA.transfer(amount).to(accountB);
or, if the code prefers roles to letters, sourceAccount and destinationAccount. Scenario names make an example easier to read by turning it into a story, but a claim about readable source code has to be tested with names that source code could actually contain - otherwise the ease being demonstrated is the story’s, not the code’s. That is why the earlier article now says accountA and accountB.
Conclusion
Reading a line answers two questions and smuggles in a third. What do I read? What was meant? And - unasked - does it make sense?
The example passed the first question and failed the second: it read as two people because Alice and Bob are people, whatever the declarations said. Fixing the names answered the second and left the third untouched: an account driving a transfer is perfectly clear and still the wrong owner. Answering that took an argument about banking that no amount of reading could have supplied - and cost nothing in readability once it had been made.
Readability makes a meaning easy to form. It does not make that meaning the author’s, and it does not make the design behind it maintainable.
More
For related discussion and background, see:
- Code and Cognition - why “hard to read” has a measurable cognitive cost, and what chunks and working memory have to do with reading code
- Source Code Is Language - the subject-predicate-object order this article’s example was built to demonstrate, and what a call loses when it has no subject