Compose everywhere: function composition in JavaScript

The translation of the article has been prepared especially for the students of the JavaScript Developer.Basic course .










Introduction



It seems like the Lodash and Underscore libraries are now ubiquitous , and still have the super efficient compose method we know today .



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 concentrate 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 exactly what the compose function does and 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. Note that functions in arguments will execute 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 are 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 use the following examples to look at the _.compose function from the Lodash library.



Examples of



Let's start simple:



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


The notEmpty function 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: the findMaxForCollection function 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);
    }
}


The findMaxForCollection function needs to be tweaked 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 pluck function 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 it _.pluckexpects 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 handy method _.currythat lets you 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


The findMaxForCollection function can be read from right to left, that is, first the val property of each item in 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 the elegance of function composition once again.



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


_.filterThe same problem arises with the function 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 isActivethat takes an object and checks if the active flag is set to true.



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


A function filterwith a function isActivecan be partially applied, so we will only pass data to the getMaxIdForActiveItems function .



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:






All Articles