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;
}
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)
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);
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:
- 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.
- 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.
- Declarative programs are concise.
Top comments (13)
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" whilereduce
,map
, andfilter
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").
That is an interesting statement
versus
when called:
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
andfilter
—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."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".
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??
Describing what I want: "The sum of even numbers" versus
still reads as
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.
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
, andfilter
withif
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).
Basic rules for imperative and declarative
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.
Good post Thanks 👍
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
Great post bud, glad to see you have got back to the content creation! 💪❤️
You know why he didn't post any. 😅
Oh god ! The man drinks instant coffee 😱
my takeaway: eventually someone has to do the imperative work. well said
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
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 :)
Thanks for sharing