Why can Array.prototype.forEach not be chained?
What you want is known as method cascading via method chaining. Describing them in brief:
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");
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:
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
andfilter
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
andfilter
with cascadable functions likeforEach
(if it was cascadable) is sacrilege because it would introduce side effects in an otherwise pure transformation.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.
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]