DEV Community

Cover image for Explained: Imperative vs Declarative programming
Siddharth
Siddharth

Posted on • Originally published at blog.siddu.tech on

Explained: Imperative vs Declarative programming

You most probably have heard of declarative vs imperative programming.

You might also have looked it up, and got something like this

In computer science, declarative programming is a programming paradigm that expresses the logic of a computation without describing its control flow.

If you're like me, that makes no sense. So let's get to metaphors!

Suppose you want a coffee.

The imperative way:

I'll grab a cup from the bottom left drawer, grab some milk from the fridge, heat some milk, pour the milk into the cup, grab some coffee powder from the top shelf and pour it into the coffee, and then pick a spoon from the table and mix the coffee.

The declarative way:

Mom, I want a coffee!

Now, you want to book a cab to the office. You could either tell the driver all the exact turns and roads to take to reach the office, or you could just give them the address.

See? The imperative way is about laying out the exact steps of how to do something, while the declarative way is about just saying what we want to do.

Note: declarative programs are just an abstraction over imperative. In the end, someone needs to do the imperative work. Mom needs to make a coffee the imperative way. The cab driver needs to know the way to your office.


Alright, let's make the jump from the metaphorical world to our code and try declarative code ourselves. Here's a quick test: write a function that sums all the even numbers in an array




Time's up! I've seen many people write the answer like this:

function evenSum(numbers) {
    let result = 0;

    for (let i = 0; i < numbers.length; i++) {
        let number = numbers[i]
        if (number % 2 === 0) {
            result += number;
        }
    }

    return result;
}

Enter fullscreen mode Exit fullscreen mode

This is imperative; this is laying out every single step.

Here's a more declarative solution:

const evenSum = numbers => numbers
    .filter(i => i % 2 === 0)
    .reduce((a, b) => a + b)

Enter fullscreen mode Exit fullscreen mode

Here we are asking JavaScript to do what we want: filter out even numbers, then add them. We could make our function even more declarative by using a few more functions.

const isEven = n => n % 2 === 0;
const sum = (a, b) => a + b;
const evenSum = numbers => numbers.filter(isEven).reduce(sum);

Enter fullscreen mode Exit fullscreen mode

Note: You must have noticed by now that functional programming is a subset of declarative programming!

You must have started seeing the benefits already. If not, here's a list:

  1. Declarative programs are way more readable. I only see "filter by even and reduce by sum", not some kind of loop which I need to manually step through to make sense of.
  2. Declarative programs are reusable. In the final code we have 3 seperate functions which we can reuse throught the codebase. Since imperative code heavily depends on state it may be hard for it to be reusable.
  3. Declarative programs are concise.

Top comments (13)

Collapse
 
peerreynders profile image
peerreynders

Declarative programs are reusable.

I think this emphasizes the wrong point.

The reason to create and name a function is to communicate "what" it does or "why" it does it (while the code describes "how" it does it) even if it is only used once in the entire program.

Note: The (imperative) for loop describes computation in terms of "control flow" while reduce, map, and filter describe computation through the "transformation of values". Both still describe the "how" even though each "how" is fundamentally different. Value transformations compose in a very simple manner and due to their sequential nature can be relatively easy to follow—once you are familiar with that model of computation.

People often only create functions to remove duplication (reuse) or to create "new abstractions".

Use named functions so your code can focus on the "why" and "what" rather than on the "how" (which is why I dislike inline anonymous function expressions - they are all about the "how" without ever attempting to justify the "why").

Since imperative code heavily depends on state it may be hard for it to be reusable.

That is an interesting statement

// version A
function sumEvenValues(numbers) {
  let sum = 0;
  for (const n of numbers) if (n % 2 === 0) sum += n;
  return sum;
}
Enter fullscreen mode Exit fullscreen mode

versus

// version B
function sumEvenValues(numbers) {
  return numbers.filter(isEven).reduce(sum);
}
Enter fullscreen mode Exit fullscreen mode

when called:

const values = [1,2,3,4];
const sum = sumEvenValues(values);
Enter fullscreen mode Exit fullscreen mode

From the perspective of the call site they work exactly the same.

Sure version B may be easier to read (though reduce seems to challenge some people) but the function name gives a pretty good hint of the "what" so the short imperative implementation of version A's function body really isn't much of a challenge.

Besides their name both versions share something else in common—both are referentially transparent:

"Referential transparency is something you are probably familiar with, even if you’ve never called it that before. Put casually, it means that any function, when given the same inputs, returns the same result. More precisely, an expression is referentially transparent when it can be replaced with its value without changing the behavior of a program." (Haskell Programming from first principles — Chapter 29: IO - 29.6 Purity is losing meaning)

So while "version A" is implemented with imperative code, the result it produces does not depend on state (just like "version B").

It's all too easy to get wrapped up with reduce, map and filter—but in JavaScript those methods only exist on array types, not for iterables in general (which are directly supported by for...of but otherwise will have to be converted to an array).

Well named ("what" or "why" rather than "how"), short, referentially transparent functions will likely contribute a lot more to your code becoming "more declarative" than replacing every for loop (over an array) you come across. And as long as a function is referentially transparent, any imperative monkey business inside of it is isolated.

… a programming paradigm that expresses the logic of a computation without describing its control flow.

"Referentially transparent" functions produce a value from the inputs and that value can piped into the next function (with array functions this is achieved via method chaining)—this moves towards computation by "transformation of values" and away from computation by "control flow".

Collapse
 
siddharthshyniben profile image
Siddharth

Note: The (imperative) for loop describes computation in terms of "control flow" while reduce, map, and filter describe computation through the "transformation of values". Both still describe the "how" even though each "how" is fundamentally different. Value transformations compose in a very simple manner and due to their sequential nature can be relatively easy to follow—once you are familiar with that model of computation.

You could argue at one point that everything does show the how, but at different levels.


I did pick a bad example for my post. Maybe my code wasn't that declarative, but I found it the simplest way to explain, and doesn't go more into depth like your comment does. The fact that both are pure functions kinda breaks the whole point, I guess??

Collapse
 
peerreynders profile image
peerreynders • Edited

everything does show the how

Describing what I want: "The sum of even numbers" versus

numbers.filter(isEven).reduce(sum);
Enter fullscreen mode Exit fullscreen mode

still reads as

  • keep only the even numbers
  • then sum them up

There's a more direct correlation between the "what" and the "how" compared to the imperative version but it still comes across as a "recipe".

Compare that to SQL where you describe the rows you want in terms of the established schema and the RDBMS determines "how" to get those rows—that's more like "I want a coffee".

At its core JavaScript is still an imperative language which due to it's history can allow for a value-oriented style but perhaps that isn't enough to go "declarative"—just maybe a "little more declarative" than full on imperative.

but at different levels.

The definition you used

"… expresses the logic of a computation without describing its control flow."

pretty much banishes control flow below the source level which makes the body of version A imperative and the body of version B functional.

However if you are mixing reduce, map, and filter with if statements on the same level then the code is fundamentally still imperative—even if there are some "functional islands" here and there.

The other thing one needs to be cautious about is to not oversimplify the apparent equivalences between the imperative and functional idioms (so sometimes additional measures or dropping down to imperative may be necessary).

const idLog = (x, i) => {
  console.log(`${i}: ${x}`);
  return x;
};
const values = [7, 6, , NaN, undefined, , 1, 0];
const results = values.map(idLog);

/*
 Holes/Empties @ index 2 and 5 are
 never processed, just preserved.

 0: 7
 1: 6
 3: NaN
 4: undefined
 6: 1
 7: 0

 */
Enter fullscreen mode Exit fullscreen mode
Collapse
 
jwhenry3 profile image
Justin Henry

Basic rules for imperative and declarative

  1. Do not create a wrapper function just to document what it does (it's a good side effect of wrapper functions, but it shouldn't be the only reason)
  2. Do create a wrapper function if there is a layer of complexity that is reused in many areas
  3. If the underlying steps will be completed in many different ways, make the imperative steps clear.

I prefer imperative over declarative depending on the level of abstraction.

Writing wrapper functions are great for reuse, but when its just as clear to write each step, it allows more flexibility without sacrificing all other instances that need to remain unchanged.

Declarative can be better when you are exposing APIs or writing abstraction layers for the purpose of distribution. This implies that you are providing a controlled way to interact with the underlying imperative code, which is perfectly fine.

Collapse
 
codewithyaku profile image
CodeWithYaku

Good post Thanks 👍

Collapse
 
weltam profile image
Welly Tambunan

declarative programming is high level one. so it will be easier to read. it's basically another layer and another abstraction on top of the real machine that execute codes.

for the high level stuff, that would be the correct approach. as it's bring so many optimization that can best done by machine or interpreter or compiler. good article

Collapse
 
grahamthedev profile image
GrahamTheDev

Great post bud, glad to see you have got back to the content creation! 💪❤️

Collapse
 
posandu profile image
Posandu

You know why he didn't post any. 😅

Collapse
 
calag4n profile image
calag4n

Oh god ! The man drinks instant coffee 😱

Collapse
 
zyabxwcd profile image
Akash • Edited

my takeaway: eventually someone has to do the imperative work. well said

Collapse
 
hazannovich profile image
Hazannovich‬‏

Newbie here. The con is that you need to memorize planty of code or there is a work around? I usually solve most of my problems using 1 or 2 loops and some conditions

Collapse
 
siddharthshyniben profile image
Siddharth

Yep, true that memorizing all the declarative helper functions can be trouble. As a newbie you probably shouldn't worry about declarative code until you become proficient in your own way :)

Collapse
 
aaravrrrrrr profile image
Aarav Reddy

Thanks for sharing