2014-09-02

Preventing function signature mismatch in ES5 and ES6

In some cases, using a function (or method) with a callback can lead to surprising results – if the signature of the latter does not match the expectations of the former. This blog post explains this phenomenon and suggests fixes.

Function signature mismatch

Let’s look at an example [1]:

    > ['1','2','3'].map(parseInt)
    [1,NaN,NaN]

Here, map() expects the following signature:

    callback(element, index, array)

But parseInt() has the signature:

    parseInt(string, radix?)

It’s not a problem that parseInt’s arity is less than the 3 expected by map; JavaScript does not complain if you ignore arguments. However, index and the optional radix don’t match semantically.

Whenever you are using a library function as a callback, you are taking a risk: its signature may not match semantically, it may even change later on.

Preventing mismatch

Prevention via arrow functions

In ECMAScript 6, arrow functions [2] give you the means to be explicit about the signature of a callback, without too much verbosity:

    > ['1', '2', '3'].map(x => parseInt(x))
    [1, 2, 3]

I like using arrow functions for this purpose: it’s compact and you immediately see how the code works.

Prevention via a helper function

Another option for preventing signature mismatch is to use a higher-order helper function, e.g.:

    > ['1', '2', '3'].map(passArgs(parseInt, 0))
    [1, 2, 3]

passArgs has the following signature:

    passArgs(toFunction, argIndex0, argIndex1, ...)

The indices indicate which of the input parameters toFunction receives and in which order. The following is an implementation for ECMAScript 5.

    function passArgs(toFunction /* argIndex0, argIndex1, ... */) {
        var indexArgs = arguments;
        return function () {
            var applyArgs = new Array(indexArgs.length-1);
            for(var i=0; i < applyArgs.length; i++) {
                var index = indexArgs[i+1];
                applyArgs[i] = arguments[index];
            }
            return toFunction.apply(this, applyArgs);
        };
    }

The following is an implementation for ECMAScript 6. Note how much simpler it is, due to rest parameters and arrow functions.

    function passArgs(toFunction, ...argIndices) {
        return function (...inputArgs) {
            var passedArgs = argIndices
                .map(argIndex => inputArgs[argIndex]);
            return toFunction.apply(this, passedArgs);
        };
    }

References

  1. Pitfall: Unexpected Optional Parameters” in Speaking JavaScript
  2. ECMAScript 6: arrow functions and method definitions

No comments: