Функция повышения * в async-функцию *?

Предположим, что у меня есть функция, которая принимает генератор и возвращает другой генератор первых элементов n:

const take = function * (n, xs) {
  console.assert(n >= 0);
  let i = 0;
  for (const x of xs) {
    if (i == n) {
      break;
    }
    yield x;
    i++;
  }
};

Использование:

const evens = function * () {
  let i = 0;
  while (true) {
    yield i;
    i += 2;
  }
};

for (const x of take(10, evens())) {
  console.log(x);
}

Теперь представьте, что evens также async (см. этот ответ для настройки):

const evensAsync = async function * () {
  let i = 0;
  while (true) {
    yield i;
    i += 2;
  }
};

Это, конечно, не работает с take:

const main = async () => {
  for await (const x of take(10, evensAsync())) {
    console.log(x);
  }
};

main().catch(e => console.error(e));

Теперь я могу определить вариант take, который async:

const takeAsync = async function * (n, xs) {
  console.assert(n >= 0);
  let i = 0;
  for await (const x of xs) {
    if (i == n) {
      break;
    }
    yield x;
    i++;
  }
};

const main = async () => {
  for await (const x of takeAsync(10, evensAsync())) {
    console.log(x);
  }
};

main().catch(e => console.error(e));

... но какая хлопот!

Есть ли способ автоматически "асинхронировать" функции генератора в JavaScript?

Ответ 1

Есть ли способ автоматически "асинхронировать" функции генератора в JavaScript?

Нет. Асинхронные и синхронные генераторы слишком разные. Вам понадобятся две различные реализации вашей функции take, там нет возможности.

Однако вы можете динамически выбирать, какой из них выбрать:

async function* takeAsync(asyncIterable) { … }
function* takeSync(iterable) { … }

function take(obj) {
    if (typeof obj[Symbol.asyncIterator] == "function") return takeAsync(obj);
    if (typeof obj[Symbol.iterator] == "function") return takeSync(obj);
    throw new TypeError("not an iterable object");
}

В общем случае невозможно написать функцию asyncify, которая может использоваться как asyncify(takeSync)(10, evensAsync()). Можно было бы взломать что-то вместе, что работает для takeSync и mapSync, полагаясь на то, что они выводят ровно один элемент для каждого элемента ввода, но он будет довольно хрупким и не работает для других итерационных функций, таких как filter.

Тем не менее, конечно, можно обобщить и предоставить абстракции.

function taker(n) {
  return {
    done: n > 0,
    *step(element) { /* if (n > 0) */ yield element; return taker(n-1); },
    result: null
  }
}
function mapper(fn) {
  return {
    done: false,
    *step(element) { yield fn(element); return this; }
    result: null
  }
}

function makeIterator(t) {
  return function*(iterable) {
    for (let element of iterable) {
      t = yield* t.step(element);
      if (t.done) break;
    }
    return t.result;
  };
}
function makeAsyncIterator(t) {
  return async function*(asyncIterable) {
    for await (let element of asyncIterable) {
      t = yield* t.step(element);
      if (t.done) break;
    }
    return t.result;
  };
}

(который бесстыдно срывает концепцию преобразователя - также не тестировался ни на что)