Imperative code is more readable
December 11th 2023 | ~ 5 minute read
And I'm tired of pretending it's not
Whenever I read about declarative code the authors invariably let it slip, almost as a fact of life, that declarative code is just inherently more readable than its imperative equivalent. But is it? All of this seems to me like some mantra most developers cite verbatim without actually reflecting on the matter.
First off, let's define our terms so everyone is on the same page while reading.
- Imperative code concerns itself with how to achieve tasks step-by-step. In that sense, it's explicit.
- Declarative code concerns itself with what needs to be done, rather than with how it's done. In that sense, it's implicit.
Said another way, declarative programming is a way of writing code without specifying its control flow, but rather, describing the problem to be solved using features built into the language.
To make sense of this predicament, let's take a look at an example of both. We'll implement a simple algorithm that gives us the sum of all positive and unique integers in a list. The solution is written in JavaScript, but it can be applied to any language that can be written in a declarative style.
Imperative
let sum = 0;
const seen = {};
const list = [1, -4, 5, 6, -23, 6, -47];
for (const element of list) {
if (element > 0 && seen[element] === undefined) {
sum += element;
seen[element] = element;
}
}
console.log(sum);
// output: 12
Declarative
const list = [1, -4, 5, 6, -23, 6, -47];
const sum = Array
.from(new Set(list))
.filter(element => element > 0)
.reduce((accumulator, current) => accumulator + current, 0);
console.log(sum);
// output: 12
Let's take a note of the obvious things that the imperative version tells you that the declarative one doesn't, just by reading the code.
- The
list
buffer isn't copied. - The algorithm is using additional space, that grows linearly with the input, as can be seen by using the
seen
object to keep track of duplicate integers. - The list is iterated through exactly once.
Now let's take a note at what the declarative solution implies but, critically, doesn't tell you.
Array.from()
,new Set()
and.filter()
all return copies of the inputlist
after doing their respective operations.new Set()
, in particular, contains an implicit loop and a deduplication step- The list is, in total, looped over 3 separate times, once for
new Set()
, once for.filter()
and once for.reduce()
I'm aware that the third point is essentially mute for languages that have support for iterators, not for JavaScript though.
Now, if you've never seen declarative code, chances are you're scratching your head at just what exactly it is that these methods do. This is the crux of my argument. Imperative code tells you everything that might be of interest to you, just by reading the code. Crucially, a new developer will be able to follow it step-by-step and understand what's going on if they're familiar with basic control flow structures like for
and if
. The declarative version, while shorter, doesn't tell you any of those things. Which is why it's declarative in the first place.
To quickly go over these methods without boring anyone to death:
Array.from()
creates a shallow copied array from an iterable object likeSet
.new Set()
calls theSet
constructor, which creates a Set data structure from the passed array, crucially, deduplicating it in the process, since sets can only contain unique values..filter
creates a newlist
buffer containing only the elements that satisfy the specified boolean operation done on them..reduce
, well, reduces the input buffer to a single value, by adding the result of the previous iteration to anaccumulator
value.
You can learn the specifics of each one on MDN.
Don't get me wrong, declarative code has its place. I'd much rather use a declarative syntax, like HTML
, when writing user interfaces than deal with whatever the hell imperative GTK
code is trying to accomplish.
Declarative code is also more expressive if you already know what it does under the hood. I use .map
, .filter
and .reduce
all the time, but I only know what they do because I've taken the time to write them imperatively. This is a common roadblock I see many people stumble upon when learning declarative programming for the first time. If that's the case for you, understanding and implementing declarative code imperatively might be something worth looking into to get a better grasp of the concept.
If you're going to write declarative code, do some research into how your language implements declarative structures so that you always know what's going on under the hood. In the case of JavaScript specifically, declarative code is inherently less performant than its imperative equivalent for some of the reasons I've stated here. Which is why I tend to avoid chaining multiple higher order functions together if I can express the same operation using a single for
loop.
Conclusion
Finally, in hopes of adding some nuance to the discussion, I propose we don't just regurgitate the statements we see others making on the internet, but take the time to understand the concepts for ourselves. I can't tell you what to do, but I can tell you that being properly informed on the topic will work wonders improving your decision making skills and will make you a better developer in the long run.