<img height="1" width="1" style="display:none" src="https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1 https://www.facebook.com/tr?id=1063935717132479&amp;ev=PageView&amp;noscript=1 "> Bitovi Blog - UX and UI design, JavaScript and Front-end development

Understand Declarative vs. Imperative Code using Array Functions

Jonathan Spickerman

Writing declarative code can be a challenge. Grasp this key concept and build better applications using JavaScript's built-in array functions!

posted in Training, Development, JavaScript on May 25, 2021 by Jonathan Spickerman


Understand Declarative vs. Imperative Code using Array Functions

Jonathan Spickerman by Jonathan Spickerman

Declarative code: it’s a hot term in all types of programming. But what does it really mean? More importantly, how do you make the jump from understanding the concept to actually writing declarative code in your projects? 

This can be a challenging mental shift to make. Fortunately, modern versions of JavaScript make getting started with declarative code easier than ever.

Declarative vs. Imperative Language

In addition to its popularity with coders, declarative vs. imperative language has plenty of relevance to other disciplines. Consider the following sentence:

I got in my car, drove to the market, put food in my cart and paid the cashier.

The sentence above is imperative: it describes how to do something. Like a JRR Tolkien novel, it's filled with detail. However, it's missing what all of these steps add up to. This is where declarative language comes in.

I bought groceries from the grocery store.

The sentence above is declarative. It describes what you’ve done without elaborating on how you’ve done it. It's the Hemingway version of buying groceries.

Declarative language is often used to shortcut information which is already clear to the reader. Most people know the steps involved in going to the store, no need to burden them with the details. 

Code can be written in the same way, using a declarative style to quickly communicate purpose without getting bogged down in implementation. 

Never Use “For” Loops to Iterate Arrays Again

I don’t iterate arrays with for loops anymore. And with good reason: I have to read every single line of a for loop to understand what it's doing. Looping isn’t the issue, it’s the lack of context that a plain for loop provides. This is the hole that declarative code can fill, and JavaScript provides built-in functions to help us out.

Array.map()

Take a look at the example below, try to figure out what it's doing:

const numbers = [1, 2, 3, 4];
const numbersDoubled = [];
for (let i = 0; i < numbers.length; i++) {
    numbersDoubled.push(numbers[i] * 2);
}

The code above uses a for loop to iterate over the numbers array. During each iteration, the current number is multiplied by two and pushed to numbersDoubled. When the loop has finished, every value in numbersDoubled will be twice that of its corresponding value in numbers.

The solution above is functional, but you have to parse every line and then make an intuitive leap to understand what the code is doing. Can this fairly simple operation be communicated in an easier-to-understand way?

const numbers = [1, 2, 3, 4];
const numbersDoubled = numbers.map((number) => number * 2);

This solution is more declarative. Array.map() returns a new array derived from the value of the array on which it is called. Each value in the source array is passed to a callback function, where it can be transformed before being passed to the new array.

Just like going to the grocery store, the steps of looping over an array and generating a new variable are the same every time. No need to constantly re-write them!

This might seem like a trivial difference, but Array.map() communicates a lot of information that I'd otherwise need to piece together myself. With Array.map(), I know that numbersDoubled will be a new array derived from numbers, and that it will be the same length as numbers. Unless casting occurs in the callback, I can reasonably assume its values will the same type as numbers. The only thing I have to parse is the callback function.

BUT WAIT, this solution provides even more new information. Array.map() is immutable, meaning it returns a new array instead of modifying the source array. By using this Array.map(), I'm indicating that numbers will not be modified when deriving numbersDoubled.

Look at all that information! I've communicated a lot more about my code while also managing to write less of it.

Array.filter()

Consider another scenario. Instead of doubling each value in numbers, I want to create a copy that contains only even numbers. An imperative solution might look like this:

const numbers = [1, 2, 3, 4];
const evenNumbers = [];
for (let i = 0; i < numbers.length; i++) {
    if (numbers[i] % 2 === 0) {
        evenNumbers.push(numbers[i]);
    }
}
 
The for loop above iterates over the array and uses the remainder operator to determine if each number is evenly divisible by 0. If this expression is truthy, the number is pushed to evenNumbers. Like the first example, this solution works but it must be parsed to be understood.
 

Fortunately, there's Array.filter(). Similar to map, Array.filter() creates a new array by passing each value in the source array to a callback function. However, this callback must simply return true or false. If the return value is true, the value is included in the new array. If false, it is left out.

const numbers = [1, 2, 3, 4];
const evenNumbers = numbers.filter((number) => number % 2 === 0);

The solution above still uses the remainder operator to determine if number is even, but the steps of iterating over the source and populating a new array are concisely handled by the filter function.

This is a big improvement, but this operation can be even more declarative. An easy target for writing declarative code is standardizing operations: what operations in this example could be turned into a reusable function?

const isNumberEven = (number) => number % 2 === 0;
const numbers = [1, 2, 3, 4];
const evenNumbers = numbers.filter(isNumberEven);
 
The code above implements a helper function for determining if a number is evenly divisible by 0. Like going to the grocery store, there's really only one way to perform this operation. No need to restate how it's done every time!
 
In one line of code, I've communicated that evenNumbers is a constant value immutably derived from numbers, and that the filter is only including numbers which are even. That's a lot of information in very few lines.
 

As operations become more complex, the information communicated by declarative code becomes even more valuable. Let's look at another example.

Array.reduce()

This time around, I want to sum all the values in numbers. An imperative approach might look like this:

const numbers = [1, 2, 3, 4];
let numbersTotal = 0;
for (let number of numbers) {
    numbersTotal += number;
}

The code above sums the array of numbers, but it still doesn’t tell us anything about itself. I could be performing any number of actions inside this loop, the only way to find out is to read it.

const numbers = [1, 2, 3, 4];
const numbersTotal = numbers.reduce((total, number) => total += number , 0);

Array.reduce() provides important context: it says new values are being derived from the contents of any array. This new value can be of any type, but common usages include mathematical operations like the summing above. 

The syntax is the same as map and filter, but adds another argument. The 0 at the end is called the “accumulator”. Each iteration passes the accumulator into the callback function as the first argument, where it can be updated before finally being returned as the output of the reduce function. In this scenario, I am adding each number from the array to the accumulator. When complete, the result is the sum of every number in numbers!

This solution has the added benefit of updating numbersTotal to a const. Since this variable is never changes, the const keyword is more accurate than its let counterpart (which allows value reassignment)

Like the filter example, the process of adding two numbers can be made more declarative. Here's an example:

const addNumbers = (numberOne, numberTwo) => numberOne + numberTwo;
const numbers = [1, 2, 3, 4];
const numbersTotal = numbers.reduce(addNumbers, 0);


The Big Picture

Let's look at all three of these operations performed imperatively:

const numbers = [1, 2, 3, 4];
 
const numbersDoubled = [];
for (let i = 0; i < numbers.length; i++) {
    numbersDoubled.push(numbers[i] * 2);
}

const evenNumbers = [];
for (let i = 0; i < numbers.length; i++) {
    if (numbers[i] % 2 === 0) {
        evenNumbers.push(numbers[i]);
    }
}
    
let numbersTotal = 0;
for (let number of numbers) {
    numbersTotal += number;
}
 
And again, with the declarative refactor:
 
const doubleNumber = (number) => number * 2;
const isNumberEven = (number) => number % 2 === 0;
const addNumbers = (numberOne, numberTwo) => numberOne + numberTwo;
     
const numbers = [1, 2, 3, 4];
const numbersDoubled = numbers.map(doubleNumber);
const evenNumbers = numbers.filter(isNumberEven);
const numbersTotal = numbers.reduce(addNumbers, 0);
 
The imperative steps require 15 lines of code (not including line breaks). The declarative solution? 7 lines. 
 
The imperative approach is lengthy and manually sets up several virtually identical loops. On top of its length, you still have to read each line to understand what the loops are doing. Having trouble holding all these loops and variables in your head? The code above won't do much to help you out.
 
The declarative refactor accomplishes the same tasks in less-than-half the amount of code. Each operation is implemented using a built-in array function, and the functions they implement are clearly named, reusable and independently testable.
 
Wrap-up
 
Changing programming styles can be difficult, and not knowing how to make the leap can keep us from growing as developers. Leveraging built-in array functions is an easy, well-supported entry point into declarative programming.

Declarative code will make your projects easier-to-read, more self-documenting and easier to test. As if you need any more reasons, it's also a fantastic entry point into functional programming (but we'll save that for another blog).
 
Write better code and build better apps, harness the power of declarative code!
Create better web applications. We’ll help. Let’s work together.