Compose everywhere: function composition in JavaScript

The translation of the article was prepared especially for the students of the course “JavaScript Developer.Basic”


Introduction

It seems libraries Lodash and Underscore are now used all over the place, and still have the super efficient method we know today compose

Let’s take a closer look at the compose function and see how it can make your code more readable, maintainable, and elegant.

The basics

We’ll cover a lot of Lodash functions because 1) we are not going to write our own basic algorithms – this will distract us from what I suggest to focus on; and 2) Lodash library is used by many developers and can be replaced with Underscore, any other library or your own algorithms without any problems.

Before looking at a few simple examples, let’s take a look at what the compose function does and see how to implement your own compose function if necessary.

var compose = function(f, g) {
    return function(x) {
        return f(g(x));
    };
};

This is the most basic implementation. Please note: functions in arguments will be executed from right to left… That is, the function on the right is executed first, and its result is passed to the function on the left of it.

Now let’s look at this code:

function reverseAndUpper(str) {
  var reversed = reverse(str);
  return upperCase(reversed);
}

The reverseAndUpper function first reverses the given string and then converts it to uppercase. We can rewrite this code using the basic compose function:

var reverseAndUpper = compose(upperCase, reverse);

The reverseAndUpper function can now be used:

reverseAndUpper('тест'); // ТСЕТ

We could have gotten the same result by writing the code:

function reverseAndUpper(str) {
  return upperCase(reverse(str));
}

This option looks more elegant and is easier to maintain and reuse.

The ability to quickly design functions and create data processing pipelines will come in handy in various situations when you need to transform data on the go. Now imagine that you need to pass a collection to a function, transform the elements of the collection and return the maximum value after all pipeline steps have been completed, or convert a given string to a Boolean value. The compose function allows you to combine multiple functions and thus create a more complex function.

Let’s implement a more flexible compose function that can include any number of other functions and arguments. The previous compose function we looked at only works with two functions and only takes the first argument passed. We can rewrite it like this:

var compose = function() {
  var funcs = Array.prototype.slice.call(аргументы);
 
  return funcs.reduce(function(f,g) {
    return function() {
      return f(g.apply(this, аргументы));
    };
  });
};

With a function like this, we can write code like this:

Var doSometing = compose(upperCase, reverse, doSomethingInitial);
 
doSomething('foo', 'bar');

There are many libraries that implement the compose function. We need our compose function to understand how it works. Of course, it is implemented differently in different libraries. The compose functions from different libraries do the same thing, so they are interchangeable. Now that we understand what the compose function is about, let’s take a look at the _.compose function from the Lodash library with the following examples.

Examples of

Let’s start simple:

function notEmpty(str) {
    return ! _.isEmpty(str);
}

Function notEmpty Is the negation of the value returned by the _.isEmpty function.

We can achieve the same result using the _.compose function from the Lodash library. Let’s write the function not:

function not(x) { return !x; }
 
var notEmpty = _.compose(not, _.isEmpty);

Now you can use the notEmpty function with any argument:

notEmpty('foo'); // true
notEmpty(''); // false
notEmpty(); // false
notEmpty(null); // false

This is a very simple example. Let’s take a look at something a little more complicated:
function findMaxForCollection returns the maximum value from a collection of objects with id and val (value) properties.

function findMaxForCollection(data) {
    var items = _.pluck(data, 'val');
    return Math.max.apply(null, items);
}
 
var data = [{id: 1, val: 5}, {id: 2, val: 6}, {id: 3, val: 2}];
 
findMaxForCollection(data);

The compose function can be used to solve this problem:

var findMaxForCollection = _.compose(function(xs) { return Math.max.apply(null, xs); }, _.pluck);
 
var data = [{id: 1, val: 5}, {id: 2, val: 6}, {id: 3, val: 2}];
 
findMaxForCollection(data, 'val'); // 6

There is work to do here.

_.pluck expects a collection as the first argument and a callback function as the second. Is it possible to partially apply the _.pluck function? In this case, you can use currying to change the order of the arguments.

function pluck(key) {
    return function(collection) {
        return _.pluck(collection, key);
    }
}

Function findMaxForCollection you need to twist a little more. Let’s create our own max function.

function max(xs) {
    return Math.max.apply(null, xs);
}

Now we can make our compose function more elegant:

var findMaxForCollection = _.compose(max, pluck('val'));
 
findMaxForCollection(data);

We wrote our own function pluck and can only use it with the ‘val’ property. You may not understand why you should write your own fetch method if Lodash already has a ready-made and convenient function _.pluck… The problem is that _.pluck expects a collection as the first argument, and we want to do it differently. By changing the order of the arguments, we can partially apply the function by passing the key as the first argument; the returned function will accept data.
We can refine our sampling method a little more. Lodash has a convenient method _.currywhich allows us to write our function like this:

function plucked(key, collection) {
    return _.pluck(collection, key);
}
 
var pluck = _.curry(plucked);

We just wrap the original pluck function to swap the arguments. Now pluck will return the function until it has processed all the arguments. Let’s see what the final code looks like:

function max(xs) {
    return Math.max.apply(null, xs);
}
 
function plucked(key, collection) {
    return _.pluck(collection, key);
}
 
var pluck = _.curry(plucked);
 
var findMaxForCollection = _.compose(max, pluck('val'));
 
var data = [{id: 1, val: 5}, {id: 2, val: 6}, {id: 3, val: 2}];
 
findMaxForCollection(data); // 6

Function findMaxForCollection can be read from right to left, that is, first the val property of each element of the collection is passed to the function, and then it returns the maximum of all values ​​accepted.

var findMaxForCollection = _.compose(max, pluck('val'));

This code is easier to maintain, reusable, and much more elegant. Let’s look at the last example, just to enjoy once again the elegance of function composition.

Let’s take the data from the previous example and add a new property called active. Now the data looks like this:

var data = [{id: 1, val: 5, active: true}, 
            {id: 2, val: 6, active: false }, 
            {id: 3, val: 2, active: true }];

Let’s call this function getMaxIdForActiveItems (data)… It takes a collection of objects, filters out all active objects, and returns the maximum value from the filtered ones.

function getMaxIdForActiveItems(data) {
    var filtered = _.filter(data, function(item) {
        return item.active === true;
    });
 
    var items = _.pluck(filtered, 'val');
    return Math.max.apply(null, items);
}

Can you make this code more elegant? It already has the max and pluck functions, so we just have to add a filter:

var getMaxIdForActiveItems = _.compose(max, pluck('val'), _.filter);
 
getMaxIdForActiveItems(data, function(item) {return item.active === true; }); // 5

With function _.filter the same problem arises as with _.pluck: we cannot partially apply this function because it expects a collection as its first argument. We can change the order of the arguments in the filter by wrapping the initial function:

function filter(fn) {
    return function(arr) {
        return arr.filter(fn);
    };
}

Let’s add a function isActivewhich takes an object and checks if the active flag is set to true.

function isActive(item) {
    return item.active === true;
}

Function filter with function isActive can be applied partially, so the function getMaxIdForActiveItems we will only transmit data.

var getMaxIdForActiveItems = _.compose(max, pluck('val'), filter(isActive));

Now we only need to transfer data:

getMaxIdForActiveItems(data); // 5

Now nothing prevents us from rewriting this function so that it finds the maximum value among inactive objects and returns it:

var isNotActive = _.compose(not, isActive);

var getMaxIdForNonActiveItems = _.compose(max, pluck('val'), filter(isNotActive));

Conclusion

As you may have noticed, function composition is an exciting feature that makes your code elegant and reusable. The main thing is to apply it correctly.

Links

lodash
Hey Underscore, You’re Doing It Wrong! (Hey Underscore, you’re doing it all wrong!)
@sharifsbeat

Read more:

  • Why is it an anti-pattern?

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *