Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Raising function * into async function *?

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?

like image 662
sdgfsdh Avatar asked Dec 15 '17 14:12

sdgfsdh


1 Answers

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)

like image 81
Bergi Avatar answered Nov 06 '22 05:11

Bergi