4

✔ 1. This, with iterator object, works:

let m = n => n * 2;
let it1 = [1, 2, 3].values ();
let [a1]  = it1.map (m),
  [...b1] = it1.map (m);
console.log (a1 + '', b1 + ''); // '2', '4,6'

✘ 2. Problem: This, with iterator helper object, doesn't (does not print '4,6'):

Note: Iterator helper is also an iterator.

The iterator helper is also an Iterator instance, making these helper methods chainable.

let m = n => n * 2;
let it2 = [1, 2, 3].values ().map (m);
let [a2]  = it2,
  [...b2] = it2;
console.log (a2 + '', b2 + ''); // '2'

✔ 3. Reusing the iterator, without map works too:

let it3 = [1, 2, 3].values ();
let [a3]  = it3,
  [...b3] = it3;
console.log (a3 + '', b3 + ''); // '1', '2,3'

Why code snippet 2, works differently from 1 and 3?

0

1 Answer 1

5

Short answer

You get different iterators for arr.values() and arr.values().map(). They react differently to destructuring assignments.

Any iterable can optionally declare a return() and throw() methods. These should close the iterable when called. Some of the syntactic sugar around iterators will attempt to automatically call these if available.

The iterator returned from arr.values() does not expose a .return() method.
The iterator returned from the iteration helper .map() does expose a .return() method.

const arr = [1, 2, 3];

console.log(arr.values().return);
console.log(arr.values().map(x => x).return);

A solution

Use a wrapper that will only proxy next() calls for but would not allow closing an iterator

function uncloseable(it) {
  return {
    next() {
      //forward all calls and arguments to the wrapped iterable
      return it.next.apply(it, arguments);
    },
    //make it a valid iterable by returning itself
    [Symbol.iterator]() { 
      return this; 
    }
  }; 
}


let m = n => n * 2;
let it2 = uncloseable([1, 2, 3].values ().map (m));
let [a2]  = it2,
  [...b2] = it2;
console.log (a2 + '', b2 + ''); // '2'

A similar thing can be achieved with a proxy. This might be useful if only .return()/.throw() should be blocked but everything else goes through:

Long explanation

We need to do a deep dive into the iteration protocol to get to the reason for this behaviour.

There are three methods that an iterator can implement according to the standard:

  • .next() is mandatory. It will return the current position of the iterator and advance it one step further.
  • .return() is optional. Calling it should close the iterator.
  • .throw() is optional. Calling it should indicate an error was encountered and the spec suggests the iterator is closed with this. But does not mandate it.

Quick note on terminology - the above is true for iterators. There are also iterables. For an object to be an iterable, it has to have a @@Symbol.iterator method which will return an iterator. Finally, these are not mutually exclusive - there are iterable iterators which are both.

Language features like for..of or destructuring have specific semantics assigned to them where they will implicitly try to close the iterator.

Specifically, here the array destructuring assignment specification if we examine an example like [x1, x2] = xs the following will happen:

  1. The assignment value (xs) is treated as an iterable and iterator for it is obtained.
    • When the assignment value is an iterable iterator, it will typically returns itself. That is what the iterator obtained from Array#values does. But if xs was an array (an iterable but not an iterator), then it would essentially get a brand new iterator which .values() provides.
  2. Values are drawn from the iterator to match the right-hand assignment elements (in the example, that is x1 and x2).
  3. Once all the assignment elements are satisfied and the iterator is still open (it returned done: false for the last assignment) then attempt to close it.
    • Closing the iterator is done by calling .return(). If no .return() is available, then the attempt is just a noop.

In effect, processing the example array destructuring assignment looks similar to this (simplified for illustrative purpose):

//obtain iterator
const iterator = xs[Symbol.iterator]();

let lastResult;

//assign to the first assignment element
lastResult = iterator.next();
const x1 = lastResult.value;

//assign to the second assignment element
lastResult = iterator.next();
const x2 = lastResult.value;

//assignments finished

//attempt to close the iterator
if(lastResult.done !== false && typeof iterator.return === "function") {
    iterator.return();
}

We can observe this easily by creating a custom iterator.

This will not close:

/* simple iterator that will return 1, 2, 3 */
const uncloseableIterator = {
  current: 1,
  done: false,
  next() {
    if (this.done || this.current > 3) {
      this.done = true;
      return { done: true };
    } else {
      return { value: this.current++, done: false };
    }
  },
  //make it a valid iterable by returning itself
  [Symbol.iterator]() { return this; }
}


const [a] = uncloseableIterator;
const [b] = uncloseableIterator;

console.log("values:", a, b);

This will be closed:

/* simple iterator that will return 1, 2, 3 */
const closeableIterator = {
  current: 1,
  done: false,
  next() {
    if (this.done || this.current > 3) {
      this.done = true;
      return { done: true };
    } else {
      return { value: this.current++, done: false };
    }
  },
  //expose a return() method
  return() {
    console.log("ITERATOR CLOSED");
    this.done = true;
    return { done: true }
  },
  //make it a valid iterable by returning itself
  [Symbol.iterator]() { return this; }
}


console.log("assignment to `a`");
const [a] = closeableIterator;
console.log("assignment to `b`");
const [b] = closeableIterator;

console.log("values:", a, b);

So, it is the destructuring assignment that will always try to close the iterator after using it. And Iterator helper objects (returned from Iterator#map and other iterator helper methods) expose a .next() and .return() methods and are thus closeable.

for..of

As a special note - for..of will also exhibit a similar behaviour if terminated early:

  • encountering a break statement will attempt to call the .return() method of the iterable

/* simple iterator that will return 1, 2, 3 */
const closeableIterator = {
  current: 1,
  done: false,
  next() {
    if (this.done || this.current > 3) {
      this.done = true;
      return { done: true };
    } else {
      return { value: this.current++, done: false };
    }
  },
  //expose a return() method
  return() {
    console.log("ITERATOR CLOSED WITH .return()");
    this.done = true;
    return { done: true }
  },
  //make it a valid iterable by returning itself
  [Symbol.iterator]() { return this; }
}


console.log("looping using for..of");
for (const item of closeableIterator) {
  console.log("item:", item);
  break; //finish early
}
console.log("end for..of loop");

  • encountering a throw statement will attempt to call the .throw() method of the iterable

/* simple iterator that will return 1, 2, 3 */
const closeableIterator = {
  current: 1,
  done: false,
  next() {
    if (this.done || this.current > 3) {
      this.done = true;
      return { done: true };
    } else {
      return { value: this.current++, done: false };
    }
  },
  //expose a return() method
  return() {
    console.log("ITERATOR CLOSED WITH .return()");
    this.done = true;
    return { done: true }
  },
  //expose a throw() method
  return() {
    console.log("ITERATOR CLOSED WITH .throw()");
    this.done = true;
    return { done: true }
  },
  //make it a valid iterable by returning itself
  [Symbol.iterator]() { return this; }
}


console.log("looping using for..of");
for (const item of closeableIterator) {
  console.log("item:", item);
  throw new Error("terminate early due to an error");
}
console.log("end for..of loop");

Sign up to request clarification or add additional context in comments.

5 Comments

Thank you. Now, after reading this answer, and reading some more documentation, i found a simple solution to my problem, like this: let a2 = it2.next ().value, [...b2] = it2; (destructuring is closing the iterator, so i delayed it)
Also viable here. But if you want to pass the iterable around you might want to wrap it. For example, consider if you did something like let a2 = it2.next ().value; doSomethingWIthIterable(it2); let [...b2] = it2; then doSomethingWIthIterable() might end up closing the iterator. Perhaps by accident (e.g., tries to for..of loop it to skip over some data or uses another operation that ends up closing it automatically). So, it's good to be aware of the auto-closing.
Another way to write unclosableIterator is, by using Iterator.from (). e.g. function unclosableIterator (it) { return Iterator.from ({ next: it.next.bind (it) }); }
@VenkataRaju ah, that's a cool trick :D
In the last snippet (hidden one), //expose a throw() method return() {, it should be throw ()?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.