How to peek at the next value in a Javascript Iterator

2.5k Views Asked by At

Let's say I have an iterator:

function* someIterator () {
    yield 1;
    yield 2;
    yield 3;
}

let iter = someIterator();

... that I look at the next element to be iterated:

let next = iter.next(); // {value: 1, done: false}

... and I then use the iterator in a loop:

for(let i of iterator)
    console.log(i); 
// 2
// 3

The loop will not include the element looked at. I wish to see the next element while not taking it out of the iteration series.

In other words, I wish to implement:

let next = peek(iter); // {value: 1, done: false}, or alternatively just 1

for(let i of iterator)
    console.log(i); 
// 1
// 2
// 3 

... and I wan't to do it without modifying the code for the iterable function.

What I've tried is in my answer. It works (which is why I made it an answer), but I worry that it builds an object that is more complex than it has to be. And I worry that it will not work for cases where the 'done' object is something different than { value = undefined, done = true }. So any improved answers are very much welcome.

4

There are 4 best solutions below

2
Shlang On BEST ANSWER

Just a bit different idea is to use wrapper that makes an iterator kind of eagier.

function peekable(iterator) {
  let state = iterator.next();

  const _i = (function* (initial) {
    while (!state.done) {
      const current = state.value;
      state = iterator.next();
      const arg = yield current;
    }
    return state.value;
  })()

  _i.peek = () => state;
  return _i;
}

function* someIterator () { yield 1; yield 2; yield 3; }
let iter = peekable(someIterator());

let v = iter.peek();
let peeked = iter.peek();
console.log(peeked.value);

for (let i of iter) {
  console.log(i);
}

2
pwilcox On

Instead of a peek function, I built a peeker function that calls next, removing the element from the iterator, but then adds it back in by creating an iterable function that first yields the captured element, then yields the remaining items in the iterable.

function peeker(iterator) {
    let peeked = iterator.next();
    let rebuiltIterator = function*() {
        if(peeked.done)
            return;
        yield peeked.value;
        yield* iterator;
    }
    return { peeked, rebuiltIterator };
}

function* someIterator () { yield 1; yield 2; yield 3; }
let iter = someIterator();
let peeked = peeker(iter);

console.log(peeked.peeked);
for(let i of peeked.rebuiltIterator())
    console.log(i);

0
mb21 On

Translating @Shlang's answer to AsyncIterables and TypeScript:

type PeekableAsyncIterable<T> = AsyncIterable<T> & {
  peek: () => Promise<T | null>;
}

/**
 * Adds a `peek` method to `AsyncIterable<T>`, which
 * peeks at the next element in the iterable without consuming it.
 */
export const peekable = <T>(asyncIterable: AsyncIterable<T>): PeekableAsyncIterable<T> => {
  let nextElPromise = asyncIterable[Symbol.asyncIterator]().next()

  const it = (async function * () {
    let nonEmpty = true
    while (nonEmpty) {
      const result = await nextElPromise
      nonEmpty = !result.done
      if (nonEmpty) {
        nextElPromise = asyncIterable[Symbol.asyncIterator]().next()
        yield result.value
      }
    }
  })() as unknown as PeekableAsyncIterable<T>

  it.peek = async () => {
    const result = await nextElPromise
    return result.done
      ? null // if you have a `Result<T>` or `Either<T, E>` type, use it here instead of null
      : result.value
  }

  return it
}
3
Jacob On

I'd like to contribute my way of solving this, based off of Shlang's answer, by creating a Peekable class.

Here it is in TypeScript:

class Peekable<T> implements Iterator<T, void> {
  public peek: IteratorResult<T, void>;
  constructor(private iterator: Iterator<T, void>) {
    this.peek = iterator.next();
  }
  next() {
    const curr = this.peek;
    this.peek = this.iterator.next();
    return curr;
  }
  [Symbol.iterator]() {
    return this;
  }
}

It can be used like any other iterator, only with an extra peek property that shows the result of the next iteration:

function* countDownFrom(n: number) {
  for (n; n > 0; n--) yield n;
}

const countDown = new Peekable(countDownFrom(15));
for (const num of countDown) {
  console.log(num);
  if (num === 7) {
    console.log(" The value after 7 is:", countDown.peek);
  }
}