Yusuf Birader

Ask What, Not How

It is a well-known fact that code is read far more than it is written. Maintaining code readability is a key object for every writer of software.

And so any approach which allows us to achieve this should be adopted with fervour.

In the early days of structured programming, most code was written using an imperative approach.

Imperative code describes how a problem should be solved. In other words, imperative code specifies each step to be taken by the computer to transform input to output.

Say we want to map each element of an array to its square.

Taking an imperative approach would mean using a for loop to iterate over the array while specifying the exact steps to compute the sum.

const nums = [1, 2, 3];
const result = [];

for (let i = 0; i < nums.length; i += 1) {
  result[i] = nums[i] ** 2;

A developer would not immediately know that the above code is mapping each array element to its square. In other words, we’re essentially implementing solutions from the perspective of the computer.

And so you can see the limitations of this approach.

In the real world, code is written and read by humans.

Forcing developers to operate at a lower level of abstraction than their own reasoning adds to the cognitive load of development, increases the chance of introducing bugs and makes the code harder to read.

We need to raise the level of abstraction of our code to that which matches the one which humans operate on.

This is where taking a declarative approach can help us.

Ask what, not how Declarative programming is a programming paradigm in which code describes what is required to solve the problem (and not how to do it).

As humans, we use declarative instructions all the time- it’s the level of abstraction that we reason at. If you want somebody to sit down, you say ‘Sit!’; you don’t specify each individual step.

Transforming our imperative array mapping code from earlier to take a declarative approach:

const nums = [1, 2, 3];
const result = nums.map( (num) => num ** 2);

Developers immediately know that the above code is performing some kind of mapping on the array. And we can make it clearer:

const nums = [1, 2, 3];
const square = num => num ** 2;
const result = nums.map(square);

Overall, a significant improvement in readability.

You may not have realised it but you’ve probably already come across declarative code. HTML, SQL, CSS- all take a declarative approach.

Other languages are more flexible. JavaScript for example, provides higher-order, declarative array abstractions, forEach, map, reduce whilst still keeping lower-level constructs like the humble for loop.

Remember, ask what, not how.

When to use a Declarative Approach

Declarative code is neither superior nor inferior to an imperative approach. What matters is understanding the problem space and choosing an approach that best improves readability.

A declarative approach is best taken when good high-level abstractions can be found, which can be used to abstract away significant complexity. Since this approach relies on modelling the problem space using high-level abstractions, and considers any imperative code as an implementation detail, this is commonly known as the top-down approach.

At times however, extracting high-level abstractions may add too much complexity or such abstractions may not exist at all. Here, an imperative approach may be more readable. The problem space is modelled by combining low-level abstractions; this is commonly know as the bottom-up approach.

Since it operates at a lower level, imperative code can often be more performant; however, many languages e.g. Haskell, Scheme, have in-built optimisations to improve the efficiency of declarative code.

In general, code readability should always be prioritised over micro-optimisations, except in the rare cases where performance is critical.

Final Thoughts

Improving code readability is the goal.

In most cases, declarative code is easier to read and to write. It raises the level of abstraction at which our code operates on to that closer to which we humans reason on.

Sometimes, however, natural abstractions, that minimise complexity can’t be found. At times like this, an imperative approach may be more suitable.