henit
henit

Reputation: 1170

Functional programming syntax with promise control flows

While adapting more to functional programming in javascript, I am wondering how to best (in regard to both functional practices and simple, readable code) solve the following.

I have a function that returns an alternate version of an object.

function doThis(obj) {
    return {
        ...obj,
        something: 'Changed'
    }
}

Nice! A simple function doing one thing, that can be used in various cases and compositions with other functions.

for one object

// Ex 1 - One function
let changed = doThis(original);

// Ex 2 - Many functions
let changed = andMore(doThat(doThis(original)));

// Ex 3 - Many functions, that need additional arguments
let changed = andMore(doThat(doThis(original, 'foo'), 'bar'), 'foo');
// or
let changed = andMore(
    doThat(
        doThis(original, 'foo'),
        'bar'
    ),
    'foo'
);

for an array of objects

// Ex 4 - One function
let changed = originals.map(doThis);

// Ex 5 - Many functions
let changed = originals
    .map(doThis)
    .map(doThat)
    .map(andMore);

// Ex 6
let changed = originals
    .map(original => doThis(original, 'foo'))
    .map(changed => doThat(changed, 'bar'))
    .map(changed => andMore(changed, 'foo'));

But then, the functions (for some reason) must do its thing asynchronously, so they return a Promise instead, that is resolved with the intended value instead of returned like before.

for one object

// Ex 7 - One function
doThis(original)
    .then(changed => {
        // continue
    });

// Ex 8 - Many functions
doThis(original)
    .then(doThat)
    .then(andMore)
    .then(changed => {
        // continue
    });

// Ex 9 - Many functions, that need additional arguments
doThis(original, 'foo')
    .then(changed => doThat(changed, 'bar'))
    .then(changed => andMore(changed, 'foo'))
    .then(changed => {
        // continue
    });

for an array of objects

// Ex 10 - One function
Promise.all(originals => originals.map(original => doThis(original)))
    .then(changed => {
        // continue
    });

// Ex 11 - Many functions
Promise.all(originals.map(original => doThis(original)))
    .then(changed => Promise.all(changed.map(oneChanged => doThat(oneChanged)))
    .then(changed => Promise.all(changed.map(oneChanged => andMore(oneChanged)))
    .then(changed => {
        // continue
    });

// Ex 12 - Many functions, that need additional arguments
Promise.all(originals.map(original => doThis(original, 'foo')))
    .then(changed => Promise.all(changed.map(oneChanged => doThat(oneChanged, 'bar')))
    .then(changed => Promise.all(changed.map(oneChanged => andMore(oneChanged, 'foo')))
    .then(changed => {
        // continue
    });

Even with such a simplified case of running one or three functions like this, some of the examples start getting very syntax ugly.

I guess the two main causes for this is

  1. When there is need for additional arguments
  2. The need to wrap in Promise.all for arrays of objects that return promises.

One solution for "problem 1" could be to make the functions return a function, to eliminate the need for a wrapper function everywhere the main function is used. Something like:

function doThis(option) {
    return function(obj) {
        // ...
    };
}

// Ex 6 can now be simplified to
someChain
    .map(doThis('foo'))
    .map(doThat('bar'))
    .map(andMore('foo'))

But is that good functional programming? Or is there a better way to solve that issue?

One solution for "problem 2" could be to wrap the function in a reusable function that checks if the passed argument is an object, or an array and for the latter, run the callback for each object in the array:

function oneOrMany(callback) {
    return (subject) => Array.isArray(subject) ?
        subject.map(oneSubject => callback(oneSubject))
        :
        callback(subject);
}

const doThis = oneOrMany(original => {
    return {
        ...original,
        something: 'Changed'
    };
});

Now, doThis can be used both for single objects (like in Ex 8) and arrays of objects (instead of Ex 11):

// Ex 
doThis(originals)
    .then(doThat)
    .then(andMore)
    .then(changed => {
        // continue
    });

Looks a lot simpler and more readable than Ex 11. But is that good functional programming? Or is there a better way to solve that issue?

Upvotes: 1

Views: 84

Answers (1)

Bergi
Bergi

Reputation: 664385

One solution for additional arguments could be to make the functions return a function, to eliminate the need for a wrapper function everywhere the main function is used.
But is that good functional programming? Or is there a better way to solve that issue?

This is perfectly fine. It is known as currying.

You could also do it programmatically instead of rewriting all your multi-argument functions, or do partial application explicitly at the place of the call, but that's mostly a syntactic difference.

One solution for Promise.all-wrapping arrays of objects that return promises could be to wrap the function in a reusable function that checks if the passed argument is an object, or an array and for the latter, run the callback for each object in the array. It can be used both for single objects and arrays of objects, which looks a lot simpler and more readable.
But is that good functional programming?

No, not at all. FP is about being specific with types, not writing open functions that do different things depending on what is passed. Do it explicitly instead:

function manyParallel(callback) {
    return function(subject) {
        return Promise.all(subject.map(callback));
    };
}

manyParallel(doThis)(originals)
.then(manyParallel(doThat))
.then(manyParallel(andMore))
.then(manyParallel(changed => {
    // continue
}));
// or alternatively (but different):
manyParallel(original => original
    .then(doThis)
    .then(doThat)
    .then(andMore)
)(originals)
.then(manyParallel(changed => … ));

Upvotes: 2

Related Questions