Skip to the content.

Reusing an algorithm in synchronous and asynchronous functions

Sometimes we want to reuse an algorithm with synchronous and asynchronous functions. Let’s take map as a simple example.

A naive implementation of map will be something like this.

function map(a, f) {
    const r = [];
    for (const v of a) {
        r.push(f(v));
    }
    return r;
}

console.log(map([1, 2, 3], x => x * 2));

Let’s convert convert it to an asynchronous function.

async function mapAsync(a, f) {
    const r = [];
    for (const v of a) {
        r.push(await f(v));
    }
    return r;
}

mapAsync([1, 2, 3], async x => x * 2).then(console.log);

I just marked the function as async and insert await when calling f. I may use Promise.all in actual use cases, but it’s not part of this post.

As they’re very similar, it’d be nice if we can reuse the implementations. It’ll become much cumbersome if the algorithm becomes more complex than map.

The first attempt is to use a dummy promise (or a monad).

const _ = require('lodash');

function mapM(a, f, lift) {
    return _.reduce(a, (p, v) => {
        return p.then(r => {
            return f(v).then(rv => lift(_.concat(r, [rv])));
        });
    }, lift([]));
}

With this mapM, you can write map and mapAsync like these.

function map(a, f) {
    class Identity {
        constructor(value) {
            this._value = value;
        }

        then(f) {
            return f(this._value);
        }
    }

    function identity(v) {
        return new Identity(v);
    }

    let ra;
    mapM(a, _.flow(f, identity), identity).then(r => ra = r);
    return ra;
}

console.log(map([1, 2, 3], x => x * 2));
function mapAsync(a, f) {
    return mapM(a, f, async v => v);
}

mapAsync([1, 2, 3], async x => x * 2).then(console.log);

This works, but it’s a bit redundant. Another approach would be to use a generator. Let’s define a generator function mapY.

function* mapY(a, f) {
    const r = [];
    for (const v of a) {
        r.push(yield f(v));
    }
    return r;
}

This look just like the first version of mapAsync, but has function* instead of async function, and uses yield instead of await.

Then we can define functions to run mapY synchronously or asynchronously.

function run(i) {
    function r(v) {
        const x = i.next(v);
        return x.done ? x.value : r(x.value);
    }
    return r();
}

console.log(run(mapY([1, 2, 3], x => x * 2)));
function runAsync(i) {
    function r(v) {
        const x = i.next(v);
        return x.done ? x.value : x.value.then(r);
    }
    return r();
}

runAsync(mapY([1, 2, 3], async x => x * 2)).then(console.log);