There is something undeniably satisfying about coming up with clever solutions to hard problems. There is a joy when you challenge yourself to use recursion instead of iteration, for example, or when you create elegant, cascading layers of abstraction that ensure code is never duplicated.
My favourite outlet for this kind of programming is Project Euler.
Project Euler is a repository of challenges based around advanced mathematics, meant to be solved with software. The catch is that your program should run in under a minute, on 2004-era hardware. That means that a brute-force solution often won’t cut it, and you’ll have to come up with a smarter solution.
Here’s an example:
The fun thing about these challenges is that they seem near-impossible at first (at least, to someone like me, who doesn’t have a math background), but often once you start breaking down the problem and experimenting, you arrive at a solution surprisingly quickly.
At least, sometimes. Other times, the problems remain inscrutable. Looking at you, Problem 60.
Once you’ve solved a problem, you’re able to view solutions that other solvers have posted. And oh wow, do people come up with terse, clever solutions for these things.
I’m kinda breaking the Project Euler rules here — you’re not supposed to share solutions, to avoid spoiling it for others — but don’t worry, these solutions are totally indecipherable, at least for folks like me coming from a JS background. You won’t spoil the challenge.
A Haskell solution
Haskell is a notoriously tricky functional language that allows for really concise solutions to hard problems:
A J solution
This J lang solution takes the cake for brevity. J has a rich library of operators, consisting of one or more symbols, and so you can express a ton of logic in a small amount of space:
These solutions leave me pretty awestruck. Clearly these solutions are pretty remarkable. It’s a true skill, and I’d bet the author of this solution felt tremendously pleased with themselves — and rightfully so — for coming up with it.
But, there’s a big difference between recreational programming on your own, and working collaboratively on a shared codebase. When you’re at your day job, or working on a shared open-source project, there are an entirely different set of priorities.
A good barometer: will an intern understand it?
A metric I like to use when writing code is this: will an intern / bootcamp grad / someone just starting their career struggle to understand what I’ve written?
In the context of a shared codebase, good code is simple code. Code that doesn’t do anything fancy. Code that makes minimal use of abstractions. Code that you’d use to explain fundamental concepts to novices.
Let’s look at an example of some code that does some data-munging. Which version of this function do you think is clearer to a new programmer?
Version 2 has a number of things going for it, on paper:
- Less code.
- Less duplication.
- Follows functional-programming principles, like using generic utility functions,
reduce, immutability, etc.
- More scalable, could easily handle 10+ fields instead of 2.
And yet, how much longer does it take to understand? How many more calories would be burned, as a new programmer, trying to make sense of it?
Version 2 ticks a lot of the boxes as to what “good” code is, at least in certain circles. But there is a huge readability cost for those characteristics, and in my opinion, it’s a bad deal.
To be clear, I’m not saying that Version 2 is bad, just that it prioritises the wrong things. Everything is tradeoffs, but I believe that readability is king when it comes to shared code.
Also, I am aware that neither version is ideal. This is the best example I could find, but it’s far from a perfect example.
Why is readability so important?
To understand why I think readability is such a crucial attribute of good code, let’s look at a popular open-source library, lodash.
lodash is an immensely popular tool. It’s downloaded more than 26,000,000 times a week on NPM alone, and has over 42,000 Github stars. There is something absolutely curious about it, though; It often has 0 open issues.
This has been true for years now, even when lodash was an essential library for a huge chunk of web developers. How can this be?!
Well, one reason is that the library’s primary author, John-David Dalton, is a passionate maintainer who spends a lot of his time triaging issues as they come in. But I don’t believe anyone, no matter how superhuman, can get a library this popular to 0 issues alone.
Years ago, I heard JDD on a podcast talk about how the key to managing a project like this is to encourage lots of folks to contribute to it. One of the ways he’s done this is by keeping the code at a pretty fundamental level; by using simple, basic constructs, it ensures that aspiring contributors can understand and contribute to the code, regardless of how much experience they have. I believe JDD mentions that they prefer if/else to ternaries simply because less people have experience with ternaries. When the goal is to keep the code simple, the expressive power of the ternary operator is detrimental.
This is important to keep in mind if you’re building an OSS tool, but it’s even more important if you’re working in a production codebase with other humans. Especially ones that have less experience than you.
A plethora of benefits.
Have you ever heard someone say this?
“Less code means less space for bugs to hide”
Proponents of terse, sophisticated code will argue that for every added character, you’re increasing the likelihood of inadvertently introducing a bug, as though the bugs are an invasive species, just waiting for you to type it up a new hideout.
There was going to be a fun image of lurking bugs here, but all the google results I got for “bugs hiding” were absolutely horrifying, so nevermind.
While it may be true that every character stroke increases the likelihood of a typo, those issues are easily caught and quickly resolved. The pernicious bugs— the ones that tend to break user experiences for weeks as developers pass the support ticket around like a hot potato— are often caused by too much complexity, not too many characters.
In order to debug an issue, you have to wrap your mind around what the code is doing. When you create an abstraction to reduce duplication (“Make it DRY”), you add a layer of indirection that your mind has to unpack. The harder it is for your mental model to account for every edge-case and possible state, the more likely it is that you’ll have trouble diagnosing what’s gone wrong.
“Everyone knows that debugging is twice as hard as writing a program in the first place. So if you’re as clever as you can be when you write it, how will you ever debug it?”
— Brian Kernighan, The Elements of Programming Style
The cost of abstractions.
If the goal is to reduce complexity, and abstractions add complexity, should we abolish abstractions altogether?
I even came up with a silly acronym for this idea! WET — Write Everything Twice.
Well, no. Abstractions are pervasive — loops are abstractions. Functions are abstractions. Programming languages themselves are abstractions over machine code, which itself is an abstraction over transistors flickering off and on really fast. It’s abstractions all the way down.
The key is to weigh the cost of an abstraction against its benefit. Say we’re building a React app, and we have a list of 100 things to render. We could copy/paste the same JSX 100 times, or we could map over an array and write the JSX once. The “complex” solution in this case is totally worth it, because the underlying complexity is commonly known, and the alternative would be burdensome to maintain.
As we build stuff, we make trade-off decisions like this all the time. If I have a point, it’s that we should consider these tradeoffs with the junior coder in mind; how much complexity are we adding for them? Is it worth it?
Code sometimes has to be complex, because the real world is complex and our software has to model it! We won’t always be able to write code that a junior engineer can easily parse and contribute to. Sometimes the business logic is genuinely really tricky, sometimes we have to use an API with an inscrutable interface, and so on.
I think the best way to deal with this is to try and sequester complexity. Set up clear boundaries between the simple stuff and the complex stuff. Don’t let the complexity seep into the surrounding areas.
John-David Dalton did this with lodash. According to his interview in that same podcast, the vast majority of lodash code is simple and easy-to-follow, and they’ve pushed the complex bits to a sophisticated core that handles the hard problems. This means that most contributors are spared from having to deal with that complexity, since it isn’t sprinkled across the application.
If your app is architected so that the most complex concerns are all dealt with in the same place, you can keep the overwhelming majority of your app’s surface area simple.
What if the junior engineer needs to work on that complex core? Well, good news! They have a more-senior person (you) to help guide them through it. Mentorship and education is a huge part of being a senior developer.
One of my favourite talks from React Rally last year was Chantastic’s “Hot Garbage; Clean Code is Dead”. I won’t spoil the talk (seriously, go watch it!), but one of the takeaways I took from it is that everyone suffers from impostor syndrome, and one of the problems with that is that we’re always trying to prove to each other that we know our shit. If we write really clever code that nobody else can understand, they’ll recognise how smart and capable we are!
Hopefully by this point in the blog post, if I haven’t failed spectacularly, it’s clear why that idea is flawed.
Over the past few years, my thinking has shifted. The code that impresses me is code that I can understand quickly. Turning a complex problem into simple code is an incredibly hard challenge, and so much more impressive than solving a simple problem with a complex solution.
My goal now isn’t to write clever code, my goal is to write welcoming, accessible code.
Thank you for coming to my TED talk.