At the moment, our implementation of ForEach
can only be used as the only or last step in a LINQ operations pipeline as it forces the execution of the chain of operations and returns void.
Just because ForEach
allows us to perform side effects doesn't mean that it needs to take us out of the Iterator pipeline. I'm thinking of the tap
operator that exists in many other languages and libraries as an example of allowing the "streaming" to continue.
https://rxjs.dev/api/operators/tap
https://hexdocs.pm/elixir/main/Kernel.html#tap/2
This would allow for things like this:
var blueWhalesWeight = whales
.Where(w => w.Color == "blue")
.ForEach(w => Console.WriteLine(w.Name))
.Select(w => w.Weight)
.ToList();
This would also mean that it is no longer possible to just do:
whales
.Select(w.Color == "blue")
.ForEach(w => Console.WriteLine(w.Name));
I guess at the end of the day it depends on our philosophy regarding Iterators. There is a reason why many languages choose to not implement a ForEach on their Iterator and that is because it doesn't fit very well with the abstraction. Iterators tend to encourage a more "functional" approach by creating a pipeline of operations where data comes in on one end, goes through a set of pure functions, and comes out at the other end transformed. The ForEach kind of breaks that abstraction in two ways:
- the very intent of the method is to make side effects
- it doesn't return anything
Languages have compromised with this by introducing the tap
operation, which allows for side effects, but keeps the flow of data going.
So, I guess I'm proposing that we either 1) make ForEach
into a Tap
or, if we prefer to leave ForEach
the way it is, that we introduce a Tap
extension method.