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.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • 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. – Cheery Oct 18 '14 at 02: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. – Scott Sauyet Oct 18 '14 at 15:42
  • 1
    Closely related: [Why provide an `array` argument in Javascript's `array.forEach` callback?](https://stackoverflow.com/q/39528571/1048572) – Bergi Apr 24 '21 at 23:08

3 Answers3

1

Quick search in our code:

function isAscending(array) {
    return array.every(function (e, idx, arr) {
        return (idx === 0) ? true : arr[idx-1] <= e;
    });
}
dusky
  • 1,133
  • 7
  • 12
  • Thank you. That's a very nice way to write `isAscending`. – Scott Sauyet Oct 22 '14 at 15:24
  • 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. – Scott Sauyet Oct 28 '14 at 15:46
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.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • 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. – Scott Sauyet Oct 22 '14 at 15:26
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.

Community
  • 1
  • 1
Aadit M Shah
  • 72,912
  • 30
  • 168
  • 299
  • 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`. – Scott Sauyet Oct 27 '14 at 14:59
  • 1
    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. – Aadit M Shah Oct 27 '14 at 17:54
  • 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. – Aadit M Shah Oct 27 '14 at 17:59
  • Yes, you've motivated discussions about a (somewhat different from yours) general solution on the [Ramda issues](https://github.com/ramda/ramda/issues/484) log. – Scott Sauyet Oct 28 '14 at 02:38