3

Do you have any real-world example of the use of the second and third parameters for the callback to Array.prototype.some or Array.prototype.any?

According to MDN:

callback is invoked with three arguments: the value of the element, the index of the element, and the Array object being traversed.

I've personally never used them.

I have been working for some time on the Javascript functional programming library, Ramda, and early on we made the controversial decision not to use the index and array parameters for other similar functions that we created. There are good reasons for this, which I don't need to get into here, except to say that for some functions, such as map and filter, we find such extra parameters do have some occasional utility. So we offer a second function which supplies them to your callback. (For example, map.idx(yourFunc, list).)

But I've never even considered doing so for some or every. I never imagined a practical use of these. But there is now a suggestion that we include these functions in our list of index-supporting ones.

So my question again is whether you have ever found an actual, live, real-world callback function to some or every which actually needs these parameters? If so, could you describe it?

Answers of "No, I never do," would be helpful data too, thanks.

3
  • I presume that the third argument may be used if you want to update the values of array on fly by some reasons. Or to compare current element to others. Commented Oct 18, 2014 at 2:33
  • There are lots of ways it could be used. I'm just curious to know if anyone actually does use the third parameter, or even (marginally more likely) the second one. Commented Oct 18, 2014 at 15:42
  • 1
    Closely related: Why provide an array argument in Javascript's array.forEach callback? Commented Apr 24, 2021 at 23:08

3 Answers 3

2
+100

Quick search in our code:

function isAscending(array) {
    return array.every(function (e, idx, arr) {
        return (idx === 0) ? true : arr[idx-1] <= e;
    });
}
Sign up to request clarification or add additional context in comments.

2 Comments

Thank you. That's a very nice way to write isAscending.
Bounty awarded as the only real-world example described. The dearth of them also convinces me that we were correct in not implementing them, although the answer from @Aadit has sparked some other interesting discussion on the issue.
1

I could imagine something like the following code to check whether an array is duplicate-free:

….every(function(v, i, arr) {
    return arr.indexOf(v, i+1) == -1;
})

Where is a complex expression so that you'd really have to use the arr parameter - which is no more an issue if you'd properly factor out the functionality in an own function that takes the array as an argument.

The second parameter can be useful sometimes, but I support your position that it is rather seldom used.

1 Comment

Yes, I can imagine others, too, and as with the isAscending response, anything working pair-wise with the data could use these parameters. But there are all sorts of scenarios I can imagine; whether I want to expand my API to accommodate them is a tricky question.
1

Yes, they are helpful

These extra parameters actually do come in handy, but not that often.

In the recent past, I had written a function to find all the permutations of a list of elements:

permute :: [a] -> [[a]]

For example permute [1,2,3] would be:

[ [1,2,3]
, [1,3,2]
, [2,1,3]
, [2,3,1]
, [3,1,2]
, [3,2,1]
]

The implementation of this function is quite simple:

  1. If the input is [] then return [[]]. This is the edge case.
  2. If the input is say [1,2,3]:
    1. Add 1 to every permutation of [2,3].
    2. Add 2 to every permutation of [1,3].
    3. Add 3 to every permutation of [1,2].

Of course, the function is recursive. In JavaScript, I implemented it as follows:

var permute = (function () {
    return permute;

    function permute(list) {
        if (list.length === 0) return [[]];     // edge case
        else return list.reduce(permutate, []); // list of permutations
                                                // is initially empty
    }

    function permutate(permutations, item, index, list) {
        var before = list.slice(0, index); // all the items before "item"
        var after = list.slice(index + 1); // all the items after "item"
        var rest = before.concat(after);   // all the items beside "item"
        var perms = permute(rest);         // permutations of rest

        // add item to the beginning of each permutation
        // the second argument of "map" is the "context"
        // (i.e. the "this" parameter of the callback)

        var newPerms = perms.map(concat, [item]);

        return permutations.concat(newPerms); // update the list of permutations
    }

    function concat(list) {
        return this.concat(list);
    }
}());

As you can see, I have used both the index and the list parameters of the permutate function. So, yes there are cases where these extra parameters are indeed helpful.

However, they are also problematic

However these superfluous arguments can sometimes be problematic and difficult to debug. The most common example of this problematic behavior is when map and parseInt are used together: javascript - Array#map and parseInt

alert(["1","2","3"].map(parseInt));

As you can see it produces the unexpected output [1,NaN,NaN]. The reason this happens it because the map function calls parseInt with 3 arguments (item, index and array):

parseInt("1", 0, ["1","2","3"]) // 1
parseInt("2", 1, ["1","2","3"]) // NaN
parseInt("3", 2, ["1","2","3"]) // NaN

However, the parseInt function takes 2 arguments (string and radix):

  1. First case, radix is 0 which is false. Hence default radix 10 is taken, resulting in 1.
  2. Second case, radix is 1. There is no base 1 numeral system. Hence we get NaN.
  3. Third case, radix is 2 which is valid. However there's no 3 in base 2. Hence we get NaN.

As you see, superfluous arguments can cause a lot of problems which are difficult to debug.

But, there is an alternative

So these extra arguments are helpful but they can cause a lot of problems. Fortunately, there is an easy solution to this problem.

In Haskell if you want to map over a list of values and the indices of each value then you use do it as follows:

map f (zip list [0..])

list                   :: [Foo]
[0..]                  :: [Int]
zip list [0..]         :: [(Foo, Int)]
f                      :: (Foo, Int) -> Bar
map f (zip list [0..]) :: [Bar]

You could do the same thing in JavaScript as follows:

function Maybe() {}

var Nothing = new Maybe;

Just.prototype = new Maybe;

function Just(a) {
    this.fromJust = a;
}

function iterator(f, xs) {
    var index = 0, length = xs.length;

    return function () {
        if (index < length) {
            var x = xs[index];
            var a = f(x, index++, xs);
            return new Just(a);
        } else return Nothing;
    };
}

We use a different map function:

function map(f, a) {
    var b = [];

    if (typeof a === "function") { // iterator
        for (var x = a(); x !== Nothing; x = a()) {
            var y = f(x.fromJust);
            b.push(y);
        }
    } else {                       // array
        for (var i = 0, l = a.length; i < l; i++) {
            var y = f(a[i]);
            b.push(y);
        }
    }

    return x;
}

Finally:

function decorateIndices(array) {
    return iterator(function (item, index, array) {
        return [item, index];
    }, array);
}

var xs = [1,2,3];

var ys = map(function (a) {
    var item = a[0];
    var index = a[1];
    return item + index;
}, decorateIndices(xs));

alert(ys); // 1,3,5

Similarly you can create decorateArray and decorateIndicesArray functions:

function decorateArray(array) {
    return iterator(function (item, index, array) {
        return [item, array];
    }, array);
}

function decorateIndicesArray(array) {
    return iterator(function (item, index, array) {
        return [item, index, array];
    }, array);
}

Currently in Ramda you have two separate functions map and map.idx. The above solution allows you to replace map.idx with idx such that:

var idx = decorateIndices;
var arr = decorateArray;
var idxArr = decorateIndicesArray;

map.idx(f, list) === map(f, idx(list))

This will allow you to get rid of a whole bunch of .idx functions, and variants.

To curry or not to curry

There is still one small problem to solve. This looks ugly:

var ys = map(function (a) {
    var item = a[0];
    var index = a[1];
    return item + index;
}, decorateIndices(xs));

It would be nicer to be able to write it like this instead:

var ys = map(function (item, index) {
    return item + index;
}, decorateIndices(xs));

However we removed superfluous arguments because they caused problems. Why should we add them back in? Two reasons:

  1. It looks cleaner.
  2. Sometimes you have a function written by somebody else which expects these extra arguments.

In Haskell you can use the uncurry function to solve this problem:

map (uncurry f) (zip list [0..])

list                             :: [Foo]
[0..]                            :: [Int]
zip list [0..]                   :: [(Foo, Int)]
f                                :: Foo -> Int -> Bar
uncurry                          :: (a -> b -> c) -> (a, b) -> c
uncurry f                        :: (Foo, Int) -> Bar
map (uncurry f) (zip list [0..]) :: [Bar]

In JavaScript the uncurry function is simply apply. It is implemented as follows:

function uncurry(f, context) {
    if (arguments.length < 2) context = null;

    return function (args) {
        return f.apply(context, args);
    };
}

Using uncurry we can write the above example as:

var ys = map(uncurry(function (item, index) {
    return item + index;
}), decorateIndices(xs));

This code is awesome because:

  1. Each function does only one job. Functions can be combined to do more complex work.
  2. Everything is explicit, which is a good thing according to the Zen of Python.
  3. There's no redundancy. There's only one map function, etc.

So I really hope this answer helps.

4 Comments

As all your answers are, this one is detailed and very informative. Ramda has not yet felt the need for uncurry but might well get there at some point. The basic question, though, was not whether such parameters could ever be useful for any function, but whether anyone had evidence of real-world usage for them on Array.prototype.some or Array.prototype.any.
There are not many use cases for the extra arguments for some and every. The examples that Bergi and dusky gave can be implemented without the use of the extra arguments. However, that's no reason not to have them. Having those extra arguments provides flexibility to end users.
A general solution, like the one that I described above is a good because you don't need to have separate map.idx functions, and end users have the flexibility to combine the functions however they wish. Hence they can easily create expressions equivalent to some.idx if they so require.
Yes, you've motivated discussions about a (somewhat different from yours) general solution on the Ramda issues log.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.