Raising function * into async function *?
Asked Answered
I

1

11

Suppose I have a function that takes a generator and returns another generator of the first n elements:

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

Usage like this:

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

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

Now imagine that evens is also async (see this answer for setup):

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

This, of course, does not work with take:

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

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

Now, I can define a take variant that is 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));

... but what a hassle!

Is there a way to automatically "asyncify" generator functions in JavaScript?

Invention answered 15/12, 2017 at 14:19 Comment(2)
async iteration proposal: github.com/tc39/proposal-async-iterationLamson
Possibly interesting article.Obola
A
1

Is there a way to automatically "asyncify" generator functions in JavaScript?

No. Asynchronous and synchronous generators are just too different. You will need two different implementations of your take function, there's no way around it.

You can however dynamically select which one to choose:

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");
}

It is not in general possible to write a function asyncify that could be used like asyncify(takeSync)(10, evensAsync()). One might be able to hack something together that works for takeSync and mapSync, relying on the fact that they output exactly one item per input item, but it would be pretty brittle and not work for other iteration functions like filter.

That said, it is of course possible to generalise and provide abstractions.

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;
  };
}

(which shamelessly rips off the transducer concept - also not tested for anything)

Ashlar answered 16/12, 2017 at 18:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.