Map, Where(Filter) and Reduce in Dart

Darsshan Nair
9 min readAug 8, 2022

--

Key concepts and practical usages of the three highly-utilised collection methods — with a touch of elegance and immutability.

Table of contents:

Preface
Conceptual Understanding
Motivations
Pratical Usages
Chaining Operations
Motivations to Chain Operations
Conclusions

Preface

There are very few occasions in our day-to-day development that result in us using a collection without modifying it in any way, shape or form, and with modern UIs aimed at enriching user experience, there are fewer still occasions which do not involve filtering the data based on the users’ needs.

In order to make our lives easier, the Iterable class in Dart gives a wide variety of methods that make it convenient for us to handle data in collection form.

In this article I am going to focus on the three rather more widely used of the lot: map(), where() and reduce(). This article aims to provide you with:

  • Conceptual understanding of map(), where() and reduce().
  • Motivations to use these methods more widely to ensure immutability.
  • Practical usages of map(), where() and reduce() individually.
  • Guides on how to chain these methods to achieve specific outcomes.

Conceptual Understanding

These methods are a lot easier to digest and understand when we look at it through a more simplistic lens. In their essence, the methods are an extension of the same principles and ideas behind ‘forEach’ loops.

These methods accept a set of data and a condition, and returns to us a new piece of information or a set of data that adheres to the given condition. And by this very definition, we can understand that these are all non-void functions, which mean two things, one, their products can be assigned to a variable, and two, they do not modify the original collection.

Now, let’s take a look at them individually.

map()

Consider the map() method as a transformer function. It accepts a collection, transforms it into something else based on the conditions and returns a new collection.

Figure 1: map() method conceptual illustration

The map() method takes in a collection of milk bottles and gives us back a collection of cheese slices. It only needs to be supplied with the instructions of how to turn milk into cheese and it takes care of the rest.

The map() method’s returns the same number of items as the collection supplied to it.

The map() method will always return a collection. Even if it is supplied with a collection containing only one item, it will create a collection containing a single item and return it.

where()

The where() method is conventionally named “filter()” in other languages out there, such as Java and Javascript. It accepts a collection of items and a condition, filters the collection based on the condition and returns a new collection of filtered items.

Figure 2: where() method conceptual illustration

The where() method accepts a collection of various items and returns us specific items, in this case a collection of cheese items, omitting other items. The condition supplied to it has to specify what it needs to filter out.

The where() method’s number of items returned depends on the condition.

The where() method always returns a collection. If the provided condition does not have any matching items, it still returns an empty collection. This makes it null-safe.

reduce()

The reduce() method operates in a slightly different way, it is an accumulator function.

The method operates by comparing the current value against the preceding value based on the condition it was supplied with. Thus, it always operates with two values at any given moment in time, starting with indexes 0 and 1 in a collection.

It returns us back a single value that satisfies the condition given to it instead of a collection. This value can be a standalone one from that collection, or a synthesised value of all the values within the collection.

Let’s try to make sense of all these technical mumbo jumbo through some illustrations.

Figure 3: reduce() method smallest item conceptual illustration

In the above illustration, we can see the reduce() being used to determine the smallest piece of cheese in the collection. It compares the first two cheeses and retains the smallest value between them, carrying it forward. This repetition happens till the end of the collection, determining the smallest cheese.

Figure 4: reduce() method largest item conceptual illustration

Meanwhile, here we can see the reduce() being used to determine the largest piece of cheese in the collection. It compares the first two cheeses and retains the largest value between them, carrying it forward. This repetition happens till the end of the collection, determining the largest cheese.

Figure 5: reduce() method total sum conceptual illustration

Finally, we can observe the reduce() being used to manufacture a large piece of cheese. It melts all the small pieces of cheese to make a large piece of cheese. This is an example of a synthesised result from a collection of items.

As you can see, in all three examples, it returns a single value.

Motivations

In the previous section, we have established how we could use the methods, let us briefly discuss why we should use these methods whenever possible, wherever necessary.

Dart emphasises developers to think declaratively. In the declarative programming paradigm, the main question that needs to be answered is: “What is our code trying to achieve?” rather than: “How do we achieve it?”. Declarative programming wraps the imperative “hows” through abstraction layers, giving more focus to the “whats”.

As such, declarative programming favours rather generously towards immutable states or values. Immutable states do not change the initial state they are derived or computed from. Instead, whenever a change is required, a new state is created.

This ensures the sanctity of the original state itself. This also presents a reference point for values to be compared against, to establish a change in state is in fact necessary.

map(), where() and reduce() are by design proponents of immutability, as they create a new value that can be assigned to a variable, by not mutating the original state itself.

One can achieve the same thing by using a forEach() method or a normal for loop. This, however, poses a few distinct problems.

  • forEach() is a void function, it does not return anything, thus the values cannot be directly assigned to a variable.
  • Looping through the collection with a for or forEach() and modifying the values will mutate the original state.
  • The code looks slightly verbose
  • Void functions cannot be chained as they do not have return values.

forEach() way:

map() way:

From the above code, we can observe that the map() method is a lot more succinct than a typical for-each approach.

As a conclusion to our conceptual discussion, map(), where() and reduce() is advisable to be used to ensure data immutability and to ensure the possibility of chaining operations together, making some complex computations that much simpler — elegantly.

Practical Usages

Now that we have cleared the air of uncertainties and doubt regarding the concepts, let’s get into codes and I shall give you some realistic examples on how we can use these concepts in our day-to-day programming lives.

For the purposes of all the examples moving forward in this article, we will be using the following set of data:

map()

From the data set, if we wanted to get a list of all the titles, traditionally we’d do something like this:

Notice how we have to declare a standalone array beforehand, and add it in manually.

With map(), we could simply do something like this, and avoid pre-declaration:

What if we wanted a list of strings that has both the title and the artist’s name together?

Effortlessly done with some string interpolation.

where()

Let us assume that we need to get a collection of all the rock songs from the larger collection above. Using a forEach(), it would like this:

Notice that, once again, we need to declare and initialise an empty list beforehand. We also need to invoke the add() function manually.

where() makes our lives easier.

It’s clearer to understand and succinct. No pre-declarations required.

What if we wanted to group together all the new songs post-2008? Easy.

Can our conditions be slightly more complex? Sure, let’s make them complex.

Slightly complex problem, still an easy solution.

reduce()

Let us look at a simple example for reduce() before proceeding further.

Take this list:

How can we get the sum of all the numbers in this list?

Usually in a forEach() approach, this would be the solution:

Now let us observe the same solution, using reduce():

What if we wanted to get the largest number from the list?

What if we wanted the smallest number?

Chaining Operations

Now, let us look at some combinations.

map() then where()

What if we wanted to get all the artists’ names that start with the letter A and the names have to be in uppercase from the collection above.

The first process is to make a list out of all the artists, and then it is filtered down.

where() then map()

What if we wanted to get all the songs that were released prior to 2009 and uppercase all the song titles? Let’s try it.

The first process is to filter the songs based on the year, and then make a final list out of the titles that are uppercased.

map() then reduce()

What if we wanted to get the total play length of all the tracks in the collection?

The first process is to make a list out of all the track lengths, and then summing them all up together.

Now, let’s really turn it complex.

where(), map() then reduce()

What if we wanted to get the total track lengths of all the rock songs in the collection?

We’ve taken a three-step approach here. Firstly, we’ve narrowed down the songs according to the genre, mapped a list out of their lengths, and then finally, accumulated all the lengths to get a final value.

Motivations to Chain Operations

The enormous benefit about a method that returns a value is that we can chain operations to solve sequential problems.

Chaining operations together also makes sure all the logical steps required to solve a particular problem can be associated with each other, in a single logical flow.

This greatly improves readability and offers better logical blocks and encapsulation of your code.

This also makes it easier for that block of code to be unit tested, improving the overall quality of your product.

Conclusion

Now that we’ve arrived at the end of this article, these are the takeaways from it:

  • map(), where() and reduce() make it easier for perform operations on collections
  • map() is a sort of transformer method that accepts n number of items, modifies it, and gives us back n number of items.
  • where() filters out whatever we need from a collection and gives us back a new collection.
  • reduce() gives us back an accumulation from the collection based on the condition, may it summation, minimum or even maximum, but it will always be a single value.
  • We can chain map(), where() and reduce() together to achieve specific goals.
  • Using these methods ensures immutability as it returns a value and can be assigned directly to a variable without the need to modify the original collection.
  • Approaches using map(), where() and reduce() promotes declarative programming.
  • They improve testability greatly by encapsulating logical flow in a proper sequence and can be unit tested directly.

--

--

Darsshan Nair

Software Engineer — Passion for Software Development (Android, iOS, React-Native, Flutter, Java, JS), Architecture and Frameworks enthusiast, Dev-Tech Speaker