2

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 Iterators and the Iteration Protocols are a promising way to abstract specific collections. However, Iterators are also stateful. Can I still avoid side effects if I rely on immutable Iterables exclusively?

2 Answers2

2

Iterators 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 Iterators or at least use them with great care.

  • Great question, with a great answer. Also, what's a thunk? – evolutionxbox Oct 12 '16 at 11:33
  • "*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
1

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?