Things can get tricky if you start modifying the container you're iterating, especially with deletions. OTOH iterators can make this impossible (e.g. through borrowing in Rust) or at least detect "iterator invalidation" at runtime.
In certain programs it's also easy to modify the container by accident, e.g. if you call a function that triggers an event that indirectly modifies your container.
If you do arithmetic on iteration variable, especially if the loop body has conditionals, then it's harder to follow the code, and easier to make off-by-one errors or infinite loops.
Not every loop is clearer with iterators, but some idioms like `.filter()` or `.any()` are clearer and even shorter to write.
On the other hand, if you do need the detailed control, the terse version doesn't let you have it, so you have to use the verbose version.
One hides the loop (potentially doing more work because of it) and one doesn’t.
The big impact as I see it is that it removes non-important bits and keeps you from messing those up, namely picking the right variable and indexing. Each new lambda in the chain is scoped to it's inputs, not all previous inputs in the iteration.
Which is to say if you get the implementation details wrong you’ve got the implementation wrong and notice very quickly with any halfway reasonable test.
Of course the same is true in the for-loop example. But overall, there are more things to specify in the for-loop:
let car_ids = [];
for (let i = 1; i <= 5; i++) {
const car_id = `car-${i}`;
if (car_is_empty(car_id)) {
car_ids.push(car_id);
}
}
return car_ids;
Specify:- storage location for result
- loop variable
- loop variable initial value
- terminating condition (is it < or <=?)
- increment
- temp variable for producing ids
- test condition for filtering
- how to add new item to result
return (1..5)
.map{|n| "car-#{n}"}
.select{|m| car_is_empty(m)}
Specify:- min value of range
- max value of range (are ranges inclusive or exclusive?)
- how to generate id
- how to query
Now, this is a trivial example. I wouldn't argue that either way is clearly better than the other in this particular case. But, in my experience and opinion, the latter is much better as things become more complex.
Particularly when on one side of things you put inclusive and exclusive together with the max value of the range and on the other you separate them.
IME this is basically horses for courses, some people will be at home with procedural syntax and other prefer using functional concepts.
That isn't happening. The loop is over an integral range (1..5 in both examples). That integer is being hashed in the example and the hash is being used for a SIDE EFFECT. Internally, the select() is doing the exact same thing, but there is no label on the value and it's being returned out of the ostensible container function.
This is not a good example of why this pattern is bad nor of why side effects are bad.
I do agree with what you are saying, don't modify the enumerable you loop over. IIRC C# is pretty safe in that it doesn't allow modifications to the enumerable inside of a for loop or foreach loop.
I think it's interesting people say functional constructs like map don't involve looping. They do, it's just hidden from the caller of those functions.