Adrián Ferrera

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. Cheat sheet

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