DEV Community

Cover image for Understanding the Building Blocks of Declarative Methods on JavaScript Array Data Collection
Oliver Mensah
Oliver Mensah

Posted on • Updated on

Understanding the Building Blocks of Declarative Methods on JavaScript Array Data Collection

JavaScript has introduced new sets of collections; Map, Sets, etc. By collection, I mean some data structures to hold multiples of data. Some of these data structures are better suited for specific problems. And they contain elements which are iterable and are designed to give developers the freedom to use generic methods to achieve several operations to be performed on them.

Way of thinking in steps

Before the introduction of these new collections, objects and arrays were the workhorses of JavaScript. Arrays are mostly used in many cases. When I hear of the array data structure, what comes to mind is thinking in terms of steps. For instance, storing a list of people's details in an array requires you to store them at a specified index. Similarly, when retrieving data from an array( using the people's details example), one simply iterates through the array and fetches the data at the index under consideration.

Imperative way of thinking in steps

Imperative thinking focuses on how something gets done. "The how" is much concerned about data storage in intermediate variables, managing control flows with loops and conditional statements. Assuming we have an array of student objects, with each object having a name, age and email as its member property, we can use the code listed below to fetch the age of each student


let students = [
    {name: "Oliver Mensah", age: 26, email: "omens@gmail.com"},
    {name: "Nana Adu", age: 16, email: null},
    {name: "Priscila Buer", age: 23, email: "prisy@gmail.com"}
]

function getUserAges(students){
    let ages  = [];
    for(i=0; i< students.length; i++){
        let student = students[i];
        if(student.age != null){
            age = student.age
            ages.push(age)
        }
    }
    return ages
}

console.log(getUserAges(students));
Enter fullscreen mode Exit fullscreen mode

You can guess what the code above illustrates; we are only creating an intermediate variable (ages array) to hold the results. While iterating through the content of the student array, there is a sanity check before adding the data to the intermediate variable. And once the iteration is done, the result-set is returned.

In all, it is just about telling how we are retrieving the ages. There is soo much work done here.

Declarative thinking

With declarative thinking, you state your desired result without dealing with every single intricate step that gets you there. It’s about declaring what you want, and not how you want it done. In many scenarios, this allows programs to be much shorter and simpler. And they are often also easier to read.

Using a declarative approach, we can refactor our code in a more concise way.


let ages = students.map(user => {
    if(user.age != null) return user.age
})
console.log(ages);
Enter fullscreen mode Exit fullscreen mode

Here we are just getting what we want without writing our own loop, managing and accessing indexes. This is powerful, right? But under the hood, the JS engine must do some sort of iterations to access each early. Anyway, I don't know much about that.

Javascript has made it easy to write declarative code on array collection. Not to reinvent the wheel, we are going to implement these declarative methods on our own just to understand how these work under the hood. By the end, we will have a deeper knowledge of how these work. Thus, having the power to create declarative methods for collections while using other programming languages that do not have these in-built features.

A closer look at the declarative methods.

We used the map method on our array data collection to retrieve the ages of students. It is a higher-order function. Thus, a function that takes in another function. And its parameterized function is anonymous - a function which is not named. This gives us a great foundation to build on. Thus, when creating a declarative method on a collection, we will need a higher-order function and synchronous callback, thus the anonymous function. The higher-order function is applied to the collection and the callback will execute on each element.

From Imperative to Declarative

Previously, we had an imperative approach to retrieve the ages of students and then applied the declarative method to achieve the same result. We can go ahead to implement the map using the guide; higher-order function and a callback on each element.

function map(items, func){
    let result = []
    for(item of items){
        result.push(func(item))
    }
    return result
}
console.log(map(students, (student) => student.age))
Enter fullscreen mode Exit fullscreen mode

We created a function that acts on the entire data collection and another method that works on each element. Pretty simple right. Let's look at other higher-order functions.

Other Functional Methods / Higher Order Functions.

We had a look at the map function to declaratively work on the data collection. It is just one of the powerful higher-order functions that operate on array collection. From here, we are going to take a deep dive into what works under the hood of other higher-order functions; filter, reject(optional), and reduce.

Filter

You will probably get to know what this function does. Thus, it basically filters some data from the data collection based on certain conditions. For instance, we can choose to get only age below 20 years. Anytime, you have to get some data from an array collection based on certain conditions then filter higher-order function must come to mind.

Imperatively, to get only the ages greater than 20 years, this is how we will go about it;

function getUserAges(students){
    let ages  = [];
    for(i=0; i< students.length; i++){
        let student = students[i];
        if(student.age > 20){
            age = student.age
            ages.push(age)
        }
    }
    return ages
}
console.log(getUserAges(students));
Enter fullscreen mode Exit fullscreen mode

To use the ib-built declarative methods, we can call the filter method to get users with certain age.

console.log(students.filter(user => user.age > 20));

Enter fullscreen mode Exit fullscreen mode

The filter returns a new array of the passed elements in the array collection so we have to map over the result to get only the ages, not the elements themselves. This is where the power of declarative methods come in. We can chain or join as many methods together to achieve that. Improving the code to get only the ages;

console.log(
 students.filter(user => user.age > 20)
         .map(user => user.age)
);
Enter fullscreen mode Exit fullscreen mode

Taking the idea from the previous example of writing our own map function. We can adopt the same pattern here. The difference here is that there is a condition. And we know we have a function to work on the entire collection and another one to act on the individual items.

function filter(items, func){
    let result = []
   for(item of items){
        if(func(item)){
            result.push(item)
        }
    }
    return result
}
Enter fullscreen mode Exit fullscreen mode

Here we have our condition saying only if the return value of the anonymous function is true, then we can add such item to our collection. When we test this we are going to get the same result as applying the in-built filter method. We got to know that we had to map over the results to get our ages only. Let's do this ;

let studentAged20Plus = filter(students, (student) => student.age >20)    
console.log(map(studentAged20Plus, (student) =>student.age));
Enter fullscreen mode Exit fullscreen mode

Reject(Optional)

The reject function does not come with Vanilla JavaScript but it is like the filter but in reverse. So we can wrap around the filter function with the Logical Operator to reverse the outcome. Let's do this;


function reject(items, func){
    return filter(items, function (item){
        return ! func(item);
    })
}
let studentAged20Plus = reject(students, 
(student) => student.age >20)
console.log(map(studentAged20Plus, 
(student) => student.age));
Enter fullscreen mode Exit fullscreen mode

So this gives the reverse of the filter outcome.

Reduce

With reduce, we are totally producing new one item value from a list of items. It does this by accumulating each current item and later return the accumulated value.

Let say we want to get the average age of the students. With an imperative approach, you will tackle the problem in this way;

function averageAge(students){
    let sumAge  = 0;
    for(i=0; i< students.length; i++){
        let student = students[i];
        sumAge += student.age
    }
    return sumAge/students.length
}
console.log(averageAge(students))
Enter fullscreen mode Exit fullscreen mode

With in-built declarative function, we will have to use the reduce method to sum them up.

console.log(students.reduce((acc, curr) 
=> acc + curr.age, 0) / students.length);

Enter fullscreen mode Exit fullscreen mode

Here, we have our accumulator with the starting value of 0 and we keep adding the age of each item to the accumulator. The result is then divided by the total number of students. Very concise and elegant.

If we are to write this declarative method, we need to keep in mind the accumulator, the individual values, the accumulator starting value and a function to works on each element.

function reduce(items, func, initialValue){
    let accumulator = initialValue;
    for(item of items){
        accumulator = func(accumulator, item);
    }
    return accumulator;
}
console.log(reduce(students,(accum, curr)
=>accum + curr.age, 0))
Enter fullscreen mode Exit fullscreen mode


`

With the two concepts; higher-order function and callback on each element, we have been able to implement the reduce function as well.

Conclusion.

I hope you will find this approach of writing declarative methods on data collections very useful. The key takeaway here is noticing the pattern. Thus at what point should the anonymous function be called. Once you understand this, you can adopt the concepts in any programming language that allows callback implementation.

Looking forward to getting your feedback or more ideas on how to implement declarative methods on data collection. Enjoy reading.

Further Reading

Indexed Collections

Keep In Touch

Let's keep in touch as we continue to learn and explore more about software engineering. Don't forget to connect with me on Twitter or LinkedIn

Discussion (0)