Why can Array.prototype.forEach not be chained?
Asked Answered
M

2

22

I learned today that forEach() returns undefined. What a waste!

If it returned the original array, it would be far more flexible without breaking any existing code. Is there any reason forEach returns undefined.

Is there anyway to chain forEach with other methods like map & filter?

For example:

var obj = someThing.keys()
.filter(someFilter)
.forEach(passToAnotherObject)
.map(transformKeys)
.reduce(reduction)

Wouldn't work because the forEach doesn't want to play nice, requiring you to run all the methods before the forEach again to get the object in the state needed for the forEach.

Maymaya answered 24/3, 2015 at 8:37 Comment(7)
To be fair, forEach() can be replaced with map(function(item) { /* Do stuff */; return item; }) when you need this ability.Whitehouse
I agree with @Whitehouse . Why would you want forEach when you can use map to iterate?Fiduciary
@CiprianMocanu To be devil's advocate, one reason why this isn't particularly great is because you wouldn't normally expect map() to cause side effects.Whitehouse
@Whitehouse What side effects do you mean? Aside from the fact that map returns a new array (not the same one) what other side effects do you mean?Fiduciary
@CiprianMocanu Pretty much anything you would normally do inside a forEach() loop would be considered a side effect in a functional sense. For example, making DOM changes based on each element of the array or making changes to another array based on the data in the current array.Whitehouse
@Whitehouse I understand now what you mean. True, you wouldn't expect map to do that, but one solution would be to always keep the foreach at the end of the chain (if of course that is possible)Fiduciary
@Whitehouse That's a nice little workaround if it comes to it. Thanks.Maymaya
T
29

What you want is known as method cascading via method chaining. Describing them in brief:

  1. Method chaining is when a method returns an object that has another method that you immediately invoke. For example, using jQuery:

    $("#person")
        .slideDown("slow")
        .addClass("grouped")
        .css("margin-left", "11px");
    
  2. Method cascading is when multiple methods are called on the same object. For example, in some languages you can do:

    foo
        ..bar()
        ..baz();
    

    Which is equivalent to the following in JavaScript:

    foo.bar();
    foo.baz();
    

JavaScript doesn't have any special syntax for method cascading. However, you can simulate method cascading using method chaining if the first method call returns this. For example, in the following code if bar returns this (i.e. foo) then chaining is equivalent to cascading:

foo
    .bar()
    .baz();

Some methods like filter and map are chainable but not cascadable because they return a new array, but not the original array.

On the other hand the forEach function is not chainable because it doesn't return a new object. Now, the question arises whether forEach should be cascadable or not.

Currently, forEach is not cascadable. However, that's not really a problem as you can simply save the result of the intermediate array in a variable and use that later:

var arr = someThing.keys()
    .filter(someFilter);

arr.forEach(passToAnotherObject);

var obj = arr
    .map(transformKeys)
    .reduce(reduction);

Yes, this solution looks uglier than the your desired solution. However, I like it more than your code for several reasons:

  1. It is consistent because chainable methods are not mixed with cascadable methods. Hence, it promotes a functional style of programming (i.e. programming with no side effects).

    Cascading is inherently an effectful operation because you are calling a method and ignoring the result. Hence, you're calling the operation for its side effects and not for its result.

    On the other hand, chainable functions like map and filter don't have any side effects (if their input function doesn't have any side effects). They are used solely for their results.

    In my humble opinion, mixing chainable methods like map and filter with cascadable functions like forEach (if it was cascadable) is sacrilege because it would introduce side effects in an otherwise pure transformation.

  2. It is explicit. As The Zen of Python teaches us, “Explicit is better than implicit.” Method cascading is just syntactic sugar. It is implicit and it comes at a cost. The cost is complexity.

    Now, you might argue that my code looks more complex than yours. If so, you would be judging the book by its cover. In their famous paper Out of the Tar Pit, the authors Ben Moseley and Peter Marks describe different types of software complexities.

    The second biggest software complexity on their list is complexity caused by explicit concern with control flow. For example:

    var obj = someThing.keys()
        .filter(someFilter)
        .forEach(passToAnotherObject)
        .map(transformKeys)
        .reduce(reduction);
    

    The above program is explicitly concerned with control flow because you are explicit stating that .forEach(passToAnotherObject) should happen before .map(transformKeys) even though it shouldn't have any effect on the overall transformation.

    In fact, you can remove it from the equation altogether and it wouldn't make any difference:

    var obj = someThing.keys()
        .filter(someFilter)
        .map(transformKeys)
        .reduce(reduction);
    

    This suggests that the .forEach(passToAnotherObject) didn't have any business being in the equation in the first place. Since it's a side effectful operation, it should be kept separate from pure code.

    When you write it explicitly as I did above, not only are you separating pure code from side effectful code but also you can choose when to evaluate each computation. For example:

    var arr = someThing.keys()
        .filter(someFilter);
    
    var obj = arr
        .map(transformKeys)
        .reduce(reduction);
    
    arr.forEach(passToAnotherObject); // evaluate after pure computation
    

    Yes, you are still explicitly concerned with control flow. However, at least now you know that .forEach(passToAnotherObject) has nothing to do with the other transformations.

    Thus, you have eliminated some (but not all) of the complexity caused by explicit concern with control flow.

For these reasons, I believe that the current implementation of forEach is actually beneficial because it prevents you from writing code that introduces complexity due to explicit concern with control flow.

I know from personal experience from when I used to work at BrowserStack that explicit concern with control flow is a big problem in large-scale software applications. It is indeed a real world problem.

It's easy to write complex code because complex code is usually shorter (implicit) code. So it's always tempting to drop in a side effectful function like forEach in the middle of a pure computation because it requires less code refactoring.

However, in the long run it makes your program more complex. Think of what would happen a few years down the line when you quit the company that you work for and somebody else has to maintain your code. Your code now looks like:

var obj = someThing.keys()
    .filter(someFilter)
    .forEach(passToAnotherObject)
    .forEach(doSomething)
    .map(transformKeys)
    .forEach(doSomethingElse)
    .reduce(reduction);

The person reading your code now has to assume that all the additional forEach methods in your chain are essential, put in extra work to understand what each function does, figure out by herself that these extra forEach methods are not essential to compute obj, eliminate them from her mental model of your code and only concentrate on the essential parts.

That's a lot of unnecessary complexity added to your program, and you thought that it was making your program more simple.

Tiein answered 24/3, 2015 at 10:32 Comment(0)
F
3

It's easy to implement a chainable forEach function:

Array.prototype.forEachChain = function () {
    this.forEach(...arguments);
    return this;
};

const arr = [1,2,3,4];

const dbl = (v, i, a) => {
    a[i] = 2 * v;
};

arr.forEachChain(dbl).forEachChain(dbl);

console.log(arr); // [4,8,12,16]
Flask answered 25/6, 2018 at 13:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.