When implementing Partial Application in ES6, implementations naturally become quite easier to reason about as default parameters, rest parameters and arrow functions can be leveraged to provide a much more comprehensive implementation.
While on the surface this may appear insignificant, when compared to having relied almost exclusively on the arguments object and Array.prototype to provide the same functionality in ES5, the benefits become rather apparent.
For instance, consider a simple multiply function which, depending on the arity of the invocation, either computes basic multiplication against the provided parameters, or returns a partial application. That is to say, if invoked as a unary function (single argument), the function returns a partial application (a new function which multiplies by the given argument). If invoked as a variadic function (variable amount of arguments), the function returns the product of the arguments.
In ES5, we could implement such a function as follows:
| // basic partial application implementation in ES5 ... var multiply = function() { var args = Array.prototype.slice.call(arguments, 0) , product; if (args.length > 1) { return args.reduce(function(acc, curr){ return typeof curr === 'number' && (curr *= acc); }); } product = typeof args[0] === 'number' ? args[0] : 0; return function(n) {return product * n;}; }; |
View Pen
Given the above example, in order to inspect and iterate over the provided arguments, we need to rely on the Array.prototype, specifically, we need to invoke Function.prototype.call on Array prototype in order to apply the slice method so as to convert the arguments object to an Array. Additionally, we also have to account for a default value of arguments[0]
should it be omitted or NaN
.
Not only does this require a superfluous amount of code, but it also results in a more complicated implementation that becomes considerably more verbose, and as a result, more difficult to reason about; especially for developers who may not be familiar with the specific mechanisms employed within the implementation.
ES6 to the rescue …
With the introduction of default parameters, …rest parameters, and Arrow Functions (fat arrows) in ES6, the implementation of the above example can be significantly reduced, and as a result, becomes considerably easier to understand, as we can simply re-write the multiply
function as:
| // basic partial application implementation in ES6 ... const multiply = (x = 0, ...y) => { if (y.length) { return y.reduce((acc, curr) => { return typeof curr === 'number' && (acc *= curr); }, x); } return typeof x === 'number' && (i => x * i); }; |
View Pen
As can be seen, implementing the multiply
function in ES6 not only reduces the SLOC by 1/2 of the previous ES5 implementation, but more importantly, by using rest parameters, it allows us to determine and work with the functions arity in a much more natural way. Moreover, both iterating over the provided arguments and returning the partial application becomes considerably more concise simply by using arrow functions, and the need to account for undefined arguments becomes moot thanks to default parameters.
In addition, variadic invocations of such functions can also be simplified considerably using the ES6 spread operator. For example, in order to pass an Array of arguments to a function in ES5, one would need to call Function.apply
against the function, like so:
| multiply.apply(null, [1, 10, 100]); //1000 |
With ES6 spread operators, however, we can simply invoke the function directly with the given array preceded by the spread operator:
| multiply(...[1, 10, 100]); //1000 |
Simple!
Hopefully this article has shed some light on a few of the features available in ES6 which allow for writing implementations which not only read much more naturally, but can be written with considerably less mental overhead.