Как я могу узнать, завершается ли итератор javascript раньше?

Допустим, у меня есть генератор:

function* source() {
  yield "hello"; yield "world";
}

Я создаю итерацию, выполняю итерацию с циклом for, а затем вырываюсь из цикла, пока итератор полностью не завершит (возврат завершен).

function run() {
  for (let item of source()) {
    console.log(item);
    break;
  }
}

Вопрос: Как я могу узнать с итерируемой стороны, что итератор завершился досрочно?

Кажется, что нет никакой обратной связи, если вы попытаетесь сделать это непосредственно в самом генераторе:

function* source2() {
  try {
    let result = yield "hello";
    console.log("foo");
  } catch (err) {
    console.log("bar");
  }
}

... ни "foo", ни "bar" не зарегистрированы.

3 ответа

Решение

Я заметил, что машинопись определяет Iterator (lib.es2015) как:

interface Iterator<T> {
  next(value?: any): IteratorResult<T>;
  return?(value?: any): IteratorResult<T>;
  throw?(e?: any): IteratorResult<T>;
} 

Я перехватил эти методы и зарегистрировал вызовы, и кажется, что если итератор завершается раньше - по крайней мере, через for-loop- тогда return метод называется. Он также будет вызван, если потребитель выдаст ошибку. Если циклу разрешено полностью повторять итератор return не называется.

Return мотыга

Итак, я немного взломал, чтобы позволить захватить другую итерацию - поэтому мне не нужно повторно реализовывать итератор.

function terminated(iterable, cb) {
  return {
    [Symbol.iterator]() {
      const it = iterable[Symbol.iterator]();
      it.return = function (value) {
        cb(value);
        return { done: true, value: undefined };
      }
      return it;
    }
  }
}

function* source() {
  yield "hello"; yield "world";
}

function source2(){
  return terminated(source(), () => { console.log("foo") });
}


for (let item of source2()) {
  console.log(item);
  break;
}

и это работает!

Привет
Foo

удалить break и вы получите:

Привет
Мир

Проверяйте после каждого yield

Набирая этот ответ, я понял, что лучшая проблема / решение - это найти в оригинальном методе генератора.

Единственный способ передать информацию обратно в исходную итерацию - это использовать next(value), Так что, если мы выберем какое-то уникальное значение (скажем, Symbol.for("terminated")), чтобы сигнализировать об окончании, и мы изменим вышеупомянутый возврат-хак для вызова it.next(Symbol.for("terminated")):

function* source() {
  let terminated = yield "hello";
  if (terminated == Symbol.for("terminated")) {
    console.log("FooBar!");
    return;
  }
  yield "world";
}

function terminator(iterable) {
  return {
    [Symbol.iterator]() {
      const it = iterable[Symbol.iterator]();
      const $return = it.return;
      it.return = function (value) {
        it.next(Symbol.for("terminated"));
        return $return.call(it)
      }
      return it;
    }
  }
}

for (let item of terminator(source())) {
  console.log(item);
  break;
}

Успех!

Привет
FooBar!

Цепные каскады Return

Если вы соедините несколько дополнительных итераторов преобразования, то return вызовите каскады через них всех:

function* chain(source) {
  for (let item of source) { yield item; }
}

for (let item of chain(chain(terminator(source())))) {
  console.log(item);
  break
}

Привет
FooBar!

пакет

Я обернул вышеупомянутое решение как пакет. Поддерживает оба [Symbol.iterator] а также [Symbol.asyncIterator], Случай асинхронного итератора представляет особый интерес для меня, особенно когда необходимо правильно утилизировать некоторый ресурс.

Есть гораздо более простой способ сделать это: использовать блок finally.

      function *source() {
  let i;

  try {
    for(i = 0; i < 5; i++)
      yield i;
  }
  finally {
    if(i !== 5)
      console.log('  terminated early');
  }
}

console.log('First:')

for(const val of source()) {
  console.log(`  ${val}`);
}

console.log('Second:')

for(const val of source()) {
  console.log(`  ${val}`);

  if(val > 2)
    break;
}

... дает:

      First:
  0
  1
  2
  3
  4
Second:
  0
  1
  2
  3
  terminated early

Я столкнулся с аналогичной необходимостью выяснить, когда итератор завершает работу раньше срока. Принятый ответ действительно умный и, вероятно, лучший способ решить проблему в целом, но я думаю, что это решение также может быть полезно для других случаев использования.

Скажем, к примеру, у вас есть бесконечное итерацию, такие как последовательность Фибоначчи, описанной в MDN в итераторы и генераторы документации.

В любом цикле должно быть установлено условие для преждевременного выхода из цикла, как в уже приведенном решении. Но что, если вы хотите деструктурировать итерируемый объект, чтобы создать массив значений? В этом случае вы захотите ограничить количество итераций, по сути установив максимальную длину итерации.

Для этого я написал функцию под названием limitIterable, который принимает в качестве аргументов итерацию, предел итераций и необязательную функцию обратного вызова, которая должна выполняться в случае, если итератор завершает работу раньше. Возвращаемое значение - это объект Generator (который одновременно является итератором и итератором), созданный с использованием выражения функции Immediately Invoked (Generator).

Когда генератор выполняется в цикле for..of, с деструктуризацией или путем вызова метода next(), он проверяет, не iterator.next().done === true или iterationCount < iterationLimit. В случае бесконечной итерации, такой как последовательность Фибоначчи, последняя всегда будет вызывать выход из цикла while. Однако обратите внимание, что можно также установить iterationLimit, превышающий длину некоторой конечной итерации, и все по-прежнему будет работать.

В любом случае после выхода из цикла while будет проверяться самый последний результат, чтобы узнать, завершился ли итератор. Если это так, будет использоваться исходное возвращаемое значение итерации. В противном случае выполняется дополнительная функция обратного вызова, которая используется в качестве возвращаемого значения.

Обратите внимание, что этот код также позволяет пользователю передавать значения в next(), который, в свою очередь, будет передан исходной итерации (см. пример с использованием последовательности Фибоначчи MDN внутри прилагаемого фрагмента кода). Это также позволяет совершать дополнительные звонки наnext() за пределами установленного iterationLimit в функции обратного вызова.

Запустите фрагмент кода, чтобы увидеть результаты нескольких возможных вариантов использования! ВотlimitIterable код функции сам по себе:

function limitIterable(iterable, iterationLimit, callback = (itCount, result, it) => undefined) {
   // callback will be executed if iterator terminates early
   if (!(Symbol.iterator in Object(iterable))) {
      throw new Error('First argument must be iterable');
   }
   if (iterationLimit < 1 || !Number.isInteger(iterationLimit)) {
      throw new Error('Second argument must be an integer greater than or equal to 1');
   }
   if (!(callback instanceof Function)) {
      throw new Error('Third argument must be a function');
   }
   return (function* () {
      const iterator = iterable[Symbol.iterator]();
      // value passed to the first invocation of next() is always ignored, so no need to pass argument to next() outside of while loop
      let result = iterator.next();
      let iterationCount = 0;
      while (!result.done && iterationCount < iterationLimit) {
         const nextArg = yield result.value;
         result = iterator.next(nextArg);
         iterationCount++;
      }
      if (result.done) {
         // iterator has been fully consumed, so result.value will be the iterator's return value (the value present alongside done: true)
         return result.value;
      } else {
         // iteration was terminated before completion (note that iterator will still accept calls to next() inside the callback function)
         return callback(iterationCount, result, iterator);
      }
   })();
}

function limitIterable(iterable, iterationLimit, callback = (itCount, result, it) => undefined) {
   // callback will be executed if iterator terminates early
   if (!(Symbol.iterator in Object(iterable))) {
      throw new Error('First argument must be iterable');
   }
   if (iterationLimit < 1 || !Number.isInteger(iterationLimit)) {
      throw new Error('Second argument must be an integer greater than or equal to 1');
   }
   if (!(callback instanceof Function)) {
      throw new Error('Third argument must be a function');
   }
   return (function* () {
      const iterator = iterable[Symbol.iterator]();
      // value passed to the first invocation of next() is always ignored, so no need to pass argument to next() outside of while loop
      let result = iterator.next();
      let iterationCount = 0;
      while (!result.done && iterationCount < iterationLimit) {
         const nextArg = yield result.value;
         result = iterator.next(nextArg);
         iterationCount++;
      }
      if (result.done) {
         // iterator has been fully consumed, so result.value will be the iterator's return value (the value present alongside done: true)
         return result.value;
      } else {
         // iteration was terminated before completion (note that iterator will still accept calls to next() inside the callback function)
         return callback(iterationCount, result, iterator);
      }
   })();
}

// EXAMPLE USAGE //
// fibonacci function from:
//https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Iterators_and_Generators#Advanced_generators
function* fibonacci() {
   let fn1 = 0;
   let fn2 = 1;
   while (true) {
      let current = fn1;
      fn1 = fn2;
      fn2 = current + fn1;
      let reset = yield current;
      if (reset) {
         fn1 = 0;
         fn2 = 1;
      }
   }
}

console.log('String iterable with 26 characters terminated early after 10 iterations, destructured into an array. Callback reached.');
const itString = limitIterable('abcdefghijklmnopqrstuvwxyz', 10, () => console.log('callback: string terminated early'));
console.log([...itString]);
console.log('Array iterable with length 3 terminates before limit of 4 is reached. Callback not reached.');
const itArray = limitIterable([1,2,3], 4, () => console.log('callback: array terminated early?'));
for (const val of itArray) {
   console.log(val);
}

const fib = fibonacci();
const fibLimited = limitIterable(fibonacci(), 9, (itCount) => console.warn(`Iteration terminated early at fibLimited. ${itCount} iterations completed.`));
console.log('Fibonacci sequences are equivalent up to 9 iterations, as shown in MDN docs linked above.');
console.log('Limited fibonacci: 11 calls to next() but limited to 9 iterations; reset on 8th call')
console.log(fibLimited.next().value);
console.log(fibLimited.next().value);
console.log(fibLimited.next().value);
console.log(fibLimited.next().value);
console.log(fibLimited.next().value);
console.log(fibLimited.next().value);
console.log(fibLimited.next().value);
console.log(fibLimited.next(true).value);
console.log(fibLimited.next().value);
console.log(fibLimited.next().value);
console.log(fibLimited.next().value);
console.log('Original (infinite) fibonacci: 11 calls to next(); reset on 8th call')
console.log(fib.next().value);
console.log(fib.next().value);
console.log(fib.next().value);
console.log(fib.next().value);
console.log(fib.next().value);
console.log(fib.next().value);
console.log(fib.next().value);
console.log(fib.next(true).value);
console.log(fib.next().value);
console.log(fib.next().value);
console.log(fib.next().value);

Другие вопросы по тегам