Differences between declarative and imperative programming

There are two main paradigms in programming: imperative and declarative. Every other paradigm, such as reactive, functional, and procedural, is just a subset of one of these two. Often, you hear that declarative code is better than imperative code and that you should give it preference. But why? Is declarative code always better than imperative code? Can it bring more problems than it solves? We'll answer all of these questions next.

The paradigms

First of all, let’s look at both paradigms to have a better understanding of how they look and behave. That way, we can evaluate them later and see their good and bad sides.

Imperative programming

Imperative programming is a programming paradigm that focuses on a step-by-step logic execution. The code in this style looks like a set of precise instructions on what needs to be done to get the desired result.

Example of imperative code in JavaScript.

const numbers = [1, 2, 3, 4, 5];

function sum(list) {
  let result = 0;

  for (let i = 0; i <= list.length; i++) {
    result += list[i];
  }

  return result;
}

In this code sum function contains step-by-step instructions on how to achieve the exact result:

  1. Create the accumulator variable.

  2. Start a loop and set the stop condition to the length of the list.

  3. Take an element by the index from the list and add it to the accumulator.

  4. Return accumulator.

Each step is carefully guided, and we need to be very precise. If we set the wrong loop condition, it won't work. If we increment the loop variable i incorrectly, it won't work, and so on.

Declarative programming

Declarative programming is a programming paradigm that focuses on the final result rather than a step-by-step logic compared to imperative programming. The code in this paradigm looks like a description of a final result that we want to get instead of a precise path to it.

Example of declarative code in Javascript.

const numbers = [1, 2, 3, 4, 5];

function sum(list) {
  return list.reduce((sum, currentNumber) => sum + currentNumber, 0);
}

Notice the difference from imperative code. We do not manually declare a variable for the accumulator (even though we pass it as a second argument, which eliminates the need for manual declaration), and we do not control the iteration process. Instead, we describe the outcome we are interested in.

Problems of declarative paradigm

With all its glossiness, code written in a declarative way has its drawbacks, which are usually overlooked. This can seriously affect the overall architecture and codebase, making the code less readable and maintainable.

Efficiency

In most cases, declarative code is an abstraction over imperative code. If the abstraction has a poor design, memory leaks, or performance issues, our codebase will suffer from it as well. This is especially true if we are talking about an abstraction that doesn't come with a standard library of a language.

Even if a declarative abstraction doesn't have any design or memory leak problems, it might still lack efficiency compared to the imperative approach. Why? Because there might be no way of writing a more efficient algorithm for this abstraction and, therefore, one may have to sacrifice efficiency for the sake of a good abstraction.

Less control

Usually, if we can abstract some details and make the code more readable, it is a good choice, but not always. If we take the example of the declarative sum function, we can clearly see that there is no way we can somehow affect the iteration process. For example, if we want to stop the iteration of the array when there is a negative number and return the sum of all numbers before this number, we simply cannot do that. Sure, we can create some flag variable outside, which allows us to control the flow of the function, but it is not a declarative way at all.

Overall, the main problem of the declarative code is the abstraction itself.

Is imperative always bad?

One of the main selling points of the declarative paradigm is code readability. However, this doesn't mean that imperative code is unreadable. It all depends on how one writes it. For example, if we have imperative code that filters an array of numbers and sums all the numbers from the array, it can be written in two ways.

const numbers = [1, 2, 3, 4, 5];

function performTaksWith(list) {

  // code where we filter the numbers 
  const filteredNumbers = [];
  for () {}

  // code where we summ numbers
  let result = 0;
  for () {}
}

Every part of the logic is placed in one place, making it difficult to read and modify as needed. However, the readability can be improved if we write it in a slightly different way.

const numbers = [1, 2, 3, 4, 5];

function performTaskWith(list) {
  const filteredNumbers = filterNumbers(list);
  return sum(filteredNumbers);
}

function filterNumbers(numbers) {
  // code where we filter number numbers.
}

function sum(list) {
  // code where we sum the numbers.
}

This version is much easier to read and, therefore, maintain. It is still written in an imperative paradigm because we are providing step-by-step instructions on what needs to be done.

Conclusion

There are two main programming paradigms: imperative and declarative. Usually, code written in a declarative paradigm tends to be shorter and more readable. However, it doesn't mean that it is always better. Declarative constructions are abstractions over some imperative code. They might have bad designs or memory problems that will leak into our apps. On the other hand, imperative programs can be written in a clear and readable way, making them easy to maintain and reason about.