Functional Operators
Array Refactoring Patterns
You may have probably heard of functional programming many times, however, if you are starting in the world of development or you come from object oriented programming, you will not know with certainty what it is about.
Let's begging with a basic definition, since the aim of this term is creating a functionality based on programming methods, it receives a few entries params, in order to return a value, at this stage it doesn't exist side effects, it means that values are immutable. Functions that fulfil these requirements are called pure functions.
At programming languages as Javascript
, exists functions at the language, that make some operations:
map
, find
, filter
, reduce
, forEach
, etc, this operations are called functional operators
and have been created to work over list of values (arrays).
Are necessaries?
Of course not. The fact that they exist does not mean that these operators will be the solution to resolve every programmatic problem. However, they are a tool that will protect you from several errors as mutability of values, hoisting errors or just to improve the readability when you have enough experience.
When making TDD, it is not a good option to focus our code on a functional operators since it will close our code design to this solution, conversely, in a mature version of the code or when we some patterns match, is a good point to apply this solution and couple to our code.
Types
The most common cases for these operators are:
forEach
The most basic. It allows us to iterate over an array, where each element of the same, it will execute a function without return any value. Mostly, we will use it by the time we want to make an operation based on the iterated value, without any change. For example: print a log, save a value, send an email...
const heroes = ['Spiderman', 'Wolverine', 'Ironman', 'Hulk', 'Ciclops']; heroes.forEach((hero) => { console.log(hero); });
Another common use is mute a value that is used at the function defined as parameter. However, we should stay away from this type of actions if we truly want pure functions.
map
Unlike what forEach do, the map operator returns a value for each one of the array values. It is commonly used when we want to retrieve partial information of each elements, transforming the data to a different structure, for example in case of a backend response to a custom model.
const heroes = [ {name: 'Spiderman', mutant: false }, {name: 'Wolverine', mutant: true }, {name: 'Ironman', mutant: false }, {name: 'Ciclops', mutant: true }, {name: 'Hulk', mutant: false }, ]; const names = heroes.map((hero) => hero.name); // Spiderman, Wolverine, Ironman, Ciclops, Hulk
Receive an array of N elements and return another with N elements. Relation 1 to 1.
filter
For its part filter will execute a function which returns a boolean
, if the value is true
it will add the value to the resulting set, otherwise, it will be excluded.
const heroes = [ {name: 'Spiderman', mutant: false }, {name: 'Wolverine', mutant: true }, {name: 'Ironman', mutant: false }, {name: 'Ciclops', mutant: true }, {name: 'Hulk', mutant: false }, ]; const mutants = heroes.filter((hero) => hero.mutant); // Wolverine, Ciclops const avengers = heroes.filter((hero) => !hero.mutant); // Spiderman, Ironman, Hulk
Receive an array of N elements and return an array of M where M <= N.
find
It acts the same way as the filter operator however it will only return the first match it found as a single instance, not as an array.
const heroes = [ {name: 'Spiderman', mutant: false }, {name: 'Wolverine', mutant: true }, {name: 'Ironman', mutant: false }, {name: 'Ciclops', mutant: true }, {name: 'Hulk', mutant: false }, ]; const mutant = heroes.find((hero) => hero.mutant); // Wolverine const avenger = heroes.find((hero) => !hero.mutant); // Spiderman
Receive an array of N elements and return the first value that match with the condition.
reduce
This is the operator with the most complex concept, it allow us to operate on the full values list, returning any type of them. The main feature is that the function will be executed for each element, in addition to receiving the element itself as a parameter, it receives the value returned by the previous function. The value must be initialized as second parameter of the reduce:
const numbers = [1, 4, 10, 3]; const initValue = 0; const result = numbers.reduce((total, quantity) => total + quantity, initValue); // 18
Despite this, we can make more complex operations such as classify data, keeping the precondition about pure functions , due to in each iteration it will generate a new value that will return to the next. Let's see the example below:
const heroes = [ {name: 'Spiderman', mutant: false }, {name: 'Wolverine', mutant: true }, {name: 'Ironman', mutant: false }, {name: 'Ciclops', mutant: true }, {name: 'Hulk', mutant: false }, ]; const heroTeams = heroes.reduce((teams, hero) => { const {avengers, mutants} = teams; return hero.mutant ? { avengers, mutants:[...mutants, hero.name]} : { avengers: [...avengers, hero.name], mutants} }, {avengers: [], mutants:[]}); /* { avengers: ['Spiderman', 'Ironman', 'Hulk'], mutants: ['Wolverine', 'Ciclops'], } */
Receive an array of N elements and return one only scalar element, instead of an array.
Refactoring patterns
Now that you know the operators, lets see classical programming patterns, that will allow you to refactor your code using the operators:
map
let names = []; for (let index = 0; index < heroes.length; index++) { names.push(heroes[index].name); // names = [ ...names, heroes[index].name]; }
At the previous example we can see how just before the loop, we define an array or empty object. This is
the first smell to realize that something is not right. Subsequently, we can find tow versions, the one that does push
on the array
, or the one that tries to be a little more "purer", but without fulfilling it in its entirety.
In this case it is as simple as seeing what transformation is done on the object and moving it to a map
:
empty value definition loop write the current value
const names = heroes.map(hero => hero.name);
filter
let mutants = []; for (let index = 0; index < heroes.length; index++) { if (heroes[index].mutant) { mutants.push(heroes[index]); } }
This case fulfills the same previous pattern, however, we see that it has a condition inside the loop. This tells us that not all the values are relevant to the result:
empty value definition loop condition write the current value
const mutants = heroes.filter((hero) => hero.mutant);
find
let mutant = {}; for (let index = 0; index < heroes.length; index++) { if (heroes[index].mutant) { mutant = heroes[index]; break; } }
This variant can be a bit more subtle, perhaps using a return
in the middle of the iteration to stop its execution as soon as it finds
the desired value. However the behavior pattern is the same. When instead of a list of values based on a condition, we want
a single value.
empty value definition loop condition write the current value loop-break
const mutant = heroes.find(hero => hero.mutant);
reduce
let heroTeams = {avengers: [], mutants: []} for(let index = 0; index < heroes.length; index++) { const currentHero = heroes[index]; currentHero.mutant ? heroTeams.mutants.push(currentHero.name) : heroTeams.avengers.push(currentHero.name); }
Being the more open method to functionality, it is more difficult to identify a pattern that applies in all cases in the same way. However, if you have not tried to use any of the previous patterns, but due to certain limitations it does not quite fit, and the function it always depends on a value that is overwritten every iteration, we could say it is a reduce.
empty value definition loop operations using the current value override the current value
const heroTeams = heroes.reduce((teams, hero) => { const { avengers, mutants } = teams; return hero.mutant ? { avengers, mutants:[...mutants, hero.name] } : { avengers: [...avengers, hero.name], mutants } }, {avengers: [], mutants:[]});
Extra - bonus
PromiseAll
It is likely that in most cases where using a for or a forEach, we will be forced to handle asynchronous requests. The same thing happens with the map, however, it is not usually for exclusive use.
for(let index = 0; index < heroes.length; index++) { await Promise.resolve(`${heroes[index].name}: Avengers, Assamble!!`) }
That is why, if within an iterator we have to make asynchronous calls, which do not depend on each other, we can solve it as follows:
const callTheTeam = (hero) => Promise.resolve(`${hero.name}: Avengers, Assamble!!`); const messages = await Promise.all(heroes.map(callTheTeam)); /*[ 'Spiderman: Avengers, Assamble!!', 'Ironman: Avengers, Assamble!!', 'Hulk: Avengers, Assamble!!' ];*/
The pattern to identify would be the following:
loop async method / Promise
Conclusion
List functional operators are powerful tools that help to simplify code once functionality has been accurately defined. However, it should not be our first version or tool by default in an iterative or incomplete development on our code, so I recommend using them in a definitive version of it.
You can find a summary of what is explained in this article, in the following Cheat sheet that I hope it will be helpful for these cases.
I would like to give special thanks to Marina Prieto Ruiz for helping me with some tips and recommendations at the translation for this article even without having knowledge of software development. Without your help this would not have been possible