As a functional programmer I want to keep my main code free from side effects and shift them to the edge of the application. ES2015 Iterator
s and the Iteration Protocols
are a promising way to abstract specific collections. However, Iterator
s are also stateful. Can I still avoid side effects if I rely on immutable Iterable
s exclusively?
-
If the iterable is immutable, what side effects could the iterator possibly have? – Bergi Oct 12 '16 at 11:34
-
It is still multicast that is, you can share the `next` effect. – Oct 12 '16 at 11:58
2 Answers
Iterator
s cause observable mutations
Iterators have one essential property: They decouple the consumer from the producer of the Iterable
by serving as an intermediary. From the consumer point of view the data source is abstracted. It might be an Array
, an Object
or a Map
. This is totally opaque to the consumer. Now that the control of the iteration process is moved from the producer to the Iterator
, the latter can establish a pull mechanism, which can be lazily used by the consumer.
To manage its task an Iterator
must keep track of the iteration state. Hence, it needs to be stateful. This is per se not harmful. However, it gets harmful as soon as state changes are observable:
const xs = [1,2,3,4,5];
const foo = itor => Array.from(itor);
const itor = xs.keys();
console.log(itor.next()); // 0
// share the iterator
console.log(foo(itor)); // [2,3,4,5] => observed mutation
console.log(itor.next()) // {value: undefined, done: true} => observed mutation
These effects occur even if you only work with immutable data types.
As a functional programmer you should avoid Iterator
s or at least use them with great care.
-
-
"*Iterators seem to encourage people to mutate iterables*" - I don't see that. The spec just requires them not to break in case they are mutated during iteration, but it stays a bad practice. – Bergi Oct 12 '16 at 11:37
-
Yes, one should never share iterators. One should share the iterable - being able to create a new (local, own) iterator by calling `[Symbol.iterator]()` is the whole point of the iteration protocol. – Bergi Oct 12 '16 at 11:39
-
@Bergi People will use the iteration protocol as they see fit. I think a good language feature should be inherently safe - not only if you obey specific rules. – Oct 12 '16 at 11:54
-
@Bergi _"The spec just requires them not to break"_ - that's what I mean with "encourage". – Oct 12 '16 at 11:55
-
@evolutionxbox sorry for the delay: A [thunk](http://stackoverflow.com/a/19862784/6445533) is a nullary function (without arguments), whose only purpose is to lazily evaluate its containing computation. – Oct 12 '16 at 12:07
-
@ftor you'll never be able to save people that just don't know any better. `while(true)` can put your program in an infinite loop – sometimes intended, sometimes not – either way, there's some risk there, but I don't think languages should be trying to prevent people from using such expressions. Mutations are only naïvely seen as "always bad" – invoking the iterator and localizing mutations is perfectly acceptable/appropriate use of the interface; and it does not preclude you from writing function, side-effect-free programs. – Mulan Oct 12 '16 at 12:13
-
@naomik: `ref.next()` is a state change and this behavior is pretty implicit in my opinion. This post is maybe a bit provocative in tone but only to make a point. I spent a lot of time with that protocol only to finally realize that I can obtain a similar behavior by using pure functions, recursion and thunks but without the drawbacks. – Oct 12 '16 at 12:45
-
@naomik Working with the iteration protocol simply increases the potential and the risk of side effects. You need a greater portion of confidence in the code. Risk and benefit of a language constructs must be balanced. I think iterators do not meet this requirement. – Oct 12 '16 at 12:59
-
@ftor is it too late to say I've totally reversed my stance re: stateless iterators since my last comment? I had totally forgot about this question until I was working on something similar just recently (as you saw) ^_^ – Mulan Sep 16 '17 at 16:19
-
@naomik of course not, I do that all the time. There is a pattern that I've recognized though: Eventually I achieve it with some sort of function, mostly combinators, mostly pure. – Sep 16 '17 at 19:27
A pure iterator is dead simple. All we need is
- the current value
- a closure that advances the iterator
- a way to signal that the iterator is exhausted
- an appropriate data structure containing these properties
const ArrayIterator = xs => {
const aux = i => i in xs
? {value: xs[i], next: () => aux(i + 1), done: false}
: {done: true};
return aux(0);
};
const take = n => ix => {
const aux = ({value, next, done}, acc) =>
done ? acc
: acc.length === n ? acc
: aux(next(), acc.concat(value));
return aux(ix, []);
};
const ix = ArrayIterator([1,2,3,4,5]);
console.log(
take(3) (ix));
console.log(
ix.next().value,
ix.next().value,
ix.next().next().value)
No global state anywhere. You can implement it for any iterable data type. take
is generic, that is it works with iterators of any data type.
Can anyone please explain me why native iterators are stateful? Why do the language designer hate functional programming?