Declarative vs. Imperative (procedural)

Nir Alfasi
6 min readNov 18, 2018

Declarative & Imperative code paradigms, are common buzz-words in the tech industry. In this post we’ll discuss these programming paradigms, what’s so good/bad about them and most important: a simple practical suggestion that can turn imperative code into declarative. Ready?

Let’s start with the more intuitive paradigm: Imperative. Imagine you want to get from point A to point B, like most of us, you’ll turn your phone’s GPS on, and ask for directions.

The list of directions may looks something like the following:
* drive straight for 0.5 miles
* turn right into street X
* continue for another 0.3 miles
* turn right into street Y
* on the first intersection turn left into street Z
* drive another 0.6 miles and the destination will be to your left

Imperative coding means creating an “ordered list of instructions” that, once followed, the expected result should be calculated.

In a CS degree, students run into imperative coding examples very often. For example, let’s take the pseudo-code for BFS

(credit to: https://www.ics.uci.edu/~eppstein/161/960215.html):

unmark all vertices
choose some starting vertex x
mark x
list L = x
tree T = x
while L nonempty
choose some vertex v from front of list
visit v
for each unmarked neighbor w
mark w
add it to end of list
add edge vw to T

Pros:
* to most of us, it feels more natural to write imperative code
* once we wrote such a code, we’re familiar with it and it’s easy for us to follow it, modify if needed, etc

Cons:
* Unless someone tells you what this code is doing, you’ll have to read the whole thing, might even need to think about it for a few seconds, until you figure out what the code is doing
* If you’re not very familiar with the code, you’ll need to “re-learn” it every time you’ll encounter it, again and again until you know it almost by heart

Q: Why is this considered to be so bad?
A: Since you (usually) write code only once, but read it many times, as well as have to maintain & debug it. Optimizing for “easy-writing” is not ideal.

Q: So how do we optimize for reading/maintaining/debugging?

I’m glad you asked!

That’s where declarative code paradigm comes into the picture!
Declarative code means that the code expresses what you want to achieve without nitpicking on the details of what steps need to be taken in order to achieve it. Sounds ideal, right? that’s also why it’s so hard for most of us to accomplish.

Let’s explore a few examples:

Example 1
Shamelessly stealing from Reed

var odds = collection.Where(num => num % 2 != 0);

The intention is very clear: we want to grab elements from a collection, but not all of them: we want only the odd numbers.

Example 2
Shamelessly stealing from Mark

SELECT score FROM games WHERE id < 100;

Here we say that we want all the scores from a table called games, but only those that have id smaller than 100.

Pay close attention that in both examples we can’t tell how the result will be achieved: in the first example we don’t know how the function Where() is implemented. And on the second example we don’t have any idea how these scores will be collected, which steps will be taken in order to provide us with these scores: will it require a full table-scan? is the ID column indexed and hence more performant?

Pop quiz: consider the two approaches of writing a program “top-down” vs. “bottom-up”, which approach is Imperative and which one is Declarative?

The answer to this question also implies how can we turn imperative code into declarative: the secret is refactoring!

Why refactoring?

Again, let’s take an example: say we want to print every prime number < 1000. Let’s start with the imperative approach, we’ll use javascript:

function prms() {
let i, j;
for (i = 2; i < 1000; i++) {
for (j = 2; j < i; j++) {
if (i % j === 0) {
break;
}
}
if (j === i) {
console.log(i);
}
}
}

not complicated right? still, it requires a few moments of going over the implementation details to understand what prms() does.
Now let’s refactor it just a bit:

function printPrimesBelow(N) {
for (let i = 2; i < N; i++) {
if (isPrime(i)) {
console.log(i);
}
}
}

awesome, now it’s much clearer: just changing the name of the function already makes it clearer to the reader. Not to mention how the code became shorter and more readable, the code flow doesn’t require break and the last if-condition that we had in the previous implementation. And before some of you start yelling at me that isPrime was not implemented, here we go:

function isPrime (n) {
for (let i = 2; i < n; i++) {
if (n % i === 0) {
return false;
}
}
return true;
}

Some people dislike this kind of refactoring, they’ll tell you that the original implementation is more performant and that it’s also shorter (12 lines vs. 15 lines). These sounds like two good arguments, so why should we go through this trouble?

1. It makes our code declarative, hence more readable & easier to maintain. Only for this reason it’s worthy: one person writes code but the whole team is maintaining it, why make them work harder?

“Any fool can write code that a computer can understand. Good programmers write code that humans can understand.” — Martin Fowler, 2008.

2. Unit-tests: breaking your code to smaller units of logic, allows you to easily test each one of them. Most of the time, when you break the code, you also reduce the complexity. An example would be an imperative function that has 16 edge-cases that needs to be tested, but breaking this function into 3 smaller functions might allow you to handle only two edge-cases in each one of them, thus reducing the number of test cases you need to write from 16 to 6. This is a naive example, in reality, when there is a long function that contains many code-paths, even if you try hard to write good tests to cover them, you most likely won’t be able to cover all of them, and we all know where the bugs like to hide (hint: it’s not in the area of the code that you tested thoroughly…)

Note: unit-tests are not the purpose, they are a means to an end of making your code more reliable, and of providing you with the confidant & security to apply code changes without worrying about breaking anything. Having this kind of safety net not only gives you delight, it also improves your velocity tremendously!

Ok, but we didn’t answer the claims about length and performance:

Length: today we deal with GBs and TBs of data, another couple of lines of code are translated into a few more bytes which is practically nothing. But even if there was a real trade off between length and readability — I’d choose the latter almost every single time.

Performance: this is my favorite because most people that try to use the “performance” card don’t know much about it. The retort would be: can you benchmark it and prove your claim that the first is more performant? this will get 95% of them off your back. The other 5% that will bother to write a benchmark (which is not a trivial thing to do if you don’t know what you’re doing) will most certainly find that even if they were right, the difference between the two versions is negligible. Further, many popular languages are not very performant to begin with (JS, Python, Ruby, PHP…) which makes the “performance” claim even more ridiculous. And last, in some rare cases you will want to make your code less readable in favor of performance (real-time systems, library that is popular, batch/long running jobs that process a lot of data) but even in these cases, making your code more performant and less readable is usually the last thing you’ll do.

To sum up, we usually want to optimize for code-reading, not for writing, and since declarative code is easier to read & understand (though it might require more effort in writing), we’ll prefer to work with declarative code. That said, we usually start by writing imperative code, and then refactoring it until it becomes declarative.

Happy coding!

--

--

Nir Alfasi

“Java is to JavaScript what Car is to Carpet.” - Chris Heilmann