javascripttypescriptasynchronous

How / Is there a way to have a generic algorithm function work with both sync & async functions?


I'm trying to do this in TypeScript, but I think the question is broader than TypeScript and applies to JavaScript as well, hence both tags.

I'm writing some generic algorithm functions (e.g. Nelder-Mead optimizer, ...) that take a user-supplied function as a parameter. I would like the algorithm to support both sync and async user functions. Is there a way to do that?

So for example, let's take the following over-simplified algorithm:

function findZero(f: (x: number) => number): number {
    for(let i = 0; i < 1000; i++) {
        const fi = f(i)
        if(fi == 0) return i
    }
    return -1
}

console.log(findZero(x => 5-x))  // outputs: 5
console.log(findZero(x => new Promise<number>(resolve => resolve(5-x))))  // outputs: -1, since a Promise != 0

Now obviously, I can convert that into an async version very easily:

async function asyncFindZero(f: (x: number) => Promise<number>): Promise<number> {
    for(let i = 0; i < 1000; i++) {
        const fi = await f(i)
        if(fi == 0) return i
    }
    return -1
}

console.log(asyncFindZero(x => 5-x))  // outputs: Promise { <pending> }
console.log(await asyncFindZero(x => 5-x))  // outputs: 5
console.log(await asyncFindZero(x => new Promise<number>(resolve => resolve(5-x))))  // outputs: 5

But I would like to avoid having two almost identical functions, with just a few awaits and Promises of difference.

Is there a way to either convert or (re)write the algorithm function such that it can both:

Something with a signature like:

function findZero<MaybeAsync extends number|Promise<number>>(
    f: (x: number) => MaybeAsync,
): MaybeAsync {
    // How to write this?
}
// Such that these both work:
console.log(findZero(x => 5-x))  // outputs: 5
console.log(await findZero(x => new Promise<number>(resolve => resolve(5-x))))  // outputs: 5

Alternatively, having two separate functions would also work, since I know statically which variant I need. Something like:

function masterFindZero(/*...*/) {/*...*/}
const syncFindZero = convertToSync(masterFindZero)
const asyncFindZero = convertToAsync(masterFindZero)

The actual algorithms are obviously more complicated, but I'm hoping that once I learn the missing concepts, I can generalise it myself.


Solution

  • You can write the master function as a generator function, then run it either synchronously or asynchronously:

    function* findZero(): Generator<number, number, number> {
        for (let i = 0; i < 1000; i++) {
            const fi = yield i;
            if (fi == 0) return i;
        }
        return -1;
    }
    function findZeroSync(f: (x: number) => number): number {
        const gen = findZero();
        let step = gen.next();
        while (!step.done) {
            step = gen.next(f(step.value));
        }
        return step.value;
    }
    async function findZeroAsync(f: (x: number) => Promise<number>): Promise<number> {
        const gen = findZero();
        let step = gen.next();
        while (!step.done) {
            step = gen.next(await f(step.value));
        }
        return step.value;
    }