Reading and writing text is something humans have been doing for thousands of years. Reading and writing code, on the other hand, not so much. But how different is text from code? Or rather, ought code to be different from text?
The principle of writing text and code is the same: express ideas through the use of symbols. And even though the details are different, this underlying shared principle is what I believe provides us, the software developers, with much we can learn from writers and their millennia-long experience. Let's start with the basics.
A well formed text reads start to finish, introducing details as you go, without the need to jump back and forth between sections, pages or paragraphs.
Yet this is the very first thing that much modern code fails to satisfy. I don't know if it is Clean Code or someone else to blame, but for some reason there is this trend to wrap everything into teeny-tiny little pieces: small functions, small types, small files, small packages, etc.
And then make everything smaller again! Do that long enough on a big enough code base and instead of a comprehensive book, which you could read from start to finish, you end up with a bunch of sticky notes. Few words per note. Scattered all across the room.
isEligible(user):
return isActive(user) and isAdult(user)
isActive(user):
return user.status == ACTIVE
isAdult(user):
return user.age >= 18
eligible = isEligible(user)
That was multiple jumps, keeping context of what is connected to what, to learn a single thing, which could have simply been written as:
eligible = user.status == ACTIVE and user.age >= 18Lines should be short. But how short? Let's look at an average book. Ever noticed that most books are rectangular, i.e. taller than they are wide? Turns out there is a sound reason for that. In book design and typography the concept is called a measure.
The sweet spot for line width is around 50 to 75 characters. Anything shorter than that and the eyes have to jump back and forth too much. Anything longer and it becomes hard to track where the next line starts. Same applies to code. The 80-column limit usually gets blamed on punch cards or dated monitors with limited screen space, but typographers had settled on about that width long before the first punch card was ever made.
if order.customer.subscription.isActive and order.total > FREE_SHIPPING_THRESHOLD and not order.customer.address.isRemote:
You had to scroll to read that. This fits and reads nicely:
subscribed = order.customer.subscription.isActive
qualifies = order.total > FREE_SHIPPING_THRESHOLD
reachable = not order.customer.address.isRemote
if subscribed and qualifies and reachable:Not everything has to be a separate function. A piece of related text is perfectly well expressed as a paragraph, separated by white space around it. The same can be done in code by grouping related statements into blocks separated by empty lines.
When a block finishes and a new one starts, it tells the reader that the previous thought is complete and a new step is starting. The details of the previous block can be flushed from the brain, keeping only its result to carry into the next one.
user = fetchUser(id)
user.lastSeen = now()
save(user)
session = createSession(user)
session.expiry = now() + SESSION_TTL
save(session)
notifyLogin(user)
The same statements, grouped:
user = fetchUser(id)
user.lastSeen = now()
save(user)
session = createSession(user)
session.expiry = now() + SESSION_TTL
save(session)
notifyLogin(user)
Just from a short look, the second version tells you it does three things.
A skimmable text is one where reading only the first sentence of every paragraph still tells the same story. Ever noticed that each paragraph in a book starts with a sentence that establishes what the paragraph is about? That is what lets you find where you left off, or get the gist of a chapter without reading all of it.
The same can be applied in code. Start each block with its most defining statement, which is usually its result. If the block is complex, start it with a comment stating its purpose.
authenticate(request):
# Reject expired tokens
token = parseToken(request)
if token.expiry < now():
return DENIED
# Refresh the session
session = findSession(token)
session.expiry = now() + SESSION_TTL
save(session)
return GRANTED
Read only the comments and the returns and you already know what the story is about.
Books are phenomenal at expressing complex ideas in an easy-to-consume manner that actually reels you in as you read. They can literally paint a picture in your mind: be it a beautiful visual landscape, rough physical or mental traits of one of the characters, a complex detective plot with many twists. You name it. When it comes to software, we like to hide things for some reason. We like to hide things behind abstractions, behind implicit operations or language behaviors. We like to remove text (code) with hopes to make the overall easier to read (I guess?). But for code to be readable there has to be code to read!
All too often folks confuse how their code looks with how it reads. Just because there is more code, it does not mean that it is worse to read. No one is complaining that "Lord of the Rings" is too long. But that is what we seem to be afraid of. This leads to attempts to shorten code for the sake of brevity: clever one-liners, omitted details that are implicitly handled elsewhere, operator overloading, heavy metaprogramming. You might end up with a very short piece of code that is responsible for something that is not necessarily very simple. And simply reading the, now very short, piece of code tells you none of those details. In other words, a complex solution to a complex problem, but expressed in few words. At first this might seem like a "win". But all the parts your code leaves out as "implicit" behavior now also require implicit knowledge. Such code is no longer "fully" readable, as it requires you to know things that are not written in the code.
flat = sum(lists, [])
This flattens a list of lists. It works because sum takes a starting value of
an empty list ([]) and + concatenates lists. Neither fact is written here.
flat = []
for sublist in lists:
flat.extend(sublist)
This longer piece of code requires no prior knowledge, just following along. Everything it does is on the page.
What are your thoughts: is there something we can learn from book writers, or are we a completely different breed?