Behold the Power of Laziness With Functional Programming

Thiery Michel
Thiery MichelJune 23, 2021
#functional-programming#js

In a previous article, I introduced Checkoid a library where you combine validators like lego pieces. This article is the third and last part in a series of three that will use Checkoid internal as an example to illustrate several functional techniques.

This time we will see how checkoid is able to combine synchronous validators with asynchronous ones.

But first, let's talk about promises.

I Promise

Promises are the idiomatic way to handle asynchronous operations in JavaScript. To create a Promise, we call new Promise() and pass a callback that can resolve and reject the Promise:

const executor = (resolve, reject) => {
    setTimeout(() => {
        resolve('foo');
    }, 300);
};
const promise1 = new Promise(executor);

promise1
    .then(value => {
        console.log(value);
        // value is "foo"
        return value.toUppercase();
    })
    .then(value => {
        // value is "FOO"
    });

console.log(promise1);
// expected output: [object Promise]

And then we get back an object. We can call then to execute code after the executor was resolved, or catch to execute code after the executor was rejected. Simple and powerful, what's not to love?

Well, Promises possess one huge drawback, which makes their implementation uselessly complicated. This drawback prevents the use of some really powerful functional techniques.

Promises aren't lazy! As soon as a Promise is instantiated, it executes its callback argument. It's already too late to change its behavior. We can only react to what will happen.

Please allow me the metaphor. Let's say that you want to send a message using a paper plane. Once the plane has been thrown, you cannot change the message, you can only wait for the answer. But as long as you did not send the plane, you can correct the message. You could even unfold the paper to fold it into a boat instead. That's not all, by being lazy, by waiting before sending the paper plane, you can even wait for several planes to put them into a parcel, and sending them by mail all at once.

Laziness Is Great

If promises were lazy, it would be possible to modify their arguments, returning a new Promise that would do something different. Or even merge several promises into a single one.

Here is an alternative Promise implementation that lazily executes the executor function, and allows to concatenate two promises (i.e. concatenate the results of the promises when resolved) .

// `executor` is the same format as the argument of a promise
// type executor = (resolve, reject) => void;
const LazyPromise = executor => ({
    then: executor,
    // concat executes nothing, it just modifies the executor argument
    concat: otherPromise => {
        const newExecutor = (resolve, reject) => {
            // get own result
            executor(result1 => {
                // get result from otherPromise
                otherPromise.then(result2 => {
                    // resolve with the two concatenated result
                    resolve(result1.concat(result2));
                }, reject);
            }, reject);
        };
        // and pass the updated then into a new LazyPromise
        return LazyPromise(newExecutor),
    },
});

We can use it for instance to concatenate two promises for a string:

// here I use setTimeout for simplicity, but you could as well fetch the string, ask it to the user, or whatever, as long as you get a string in the end, this will work.
const foo = LazyPromise(resolve => setTimeout(() => resolve('foo'), 1000));
const bar = LazyPromise(resolve => setTimeout(() => resolve('bar'), 2000));

const fooBar = foo.concat(bar);
fooBar.then(result => console.log(result));
// will log `foobar` after two seconds

By the way, I just implemented an asynchronous monoid.

Asynchronous Validation

Armed with that knowledge, let's augment my Validation monoid from the first article in this series. As a reminder, it is composed of two factory functions, Valid and Invalid:

const Valid = () => ({
    isValid: true,
    and: otherValidation => (otherValidation.isValid ? Valid() : otherValidation),
    or: otherValidation => Valid(),
    format: fn => Valid(),
    getResult: () => undefined,
});

const Invalid = invalidResults => ({
    invalidResults,
    isValid: true,
    and: otherValidation => (otherValidation.isValid() ? Invalid(x) : Invalid(x.concat(otherValidation.x))),
    or: otherValidation => (otherValidation.isValid() ? Valid() : Invalid(x.concat(otherValidation.x))),
    getResult: () => invalidResults,
});

Note that this is not the validator monoid yet, but the Validation monoid that holds the validation result.

Let's add an AsyncValidation Monoid. For now, it will be composed of the set of all executor functions resolving to a Validation.

const AsyncValidation = executor => ({
    then: executor,
    and: otherAsyncValidation => {
        const newExecutor = (resolve, reject) => {
            executor(result1 => {
                otherAsyncValidation.then(result2 => resolve(result1.and(result2)), reject);
            }, reject);
        };
        return AsyncValidation(newExecutor);
    },
    or: otherAsyncValidation => {
        const newExecutor = (resolve, reject) => {
            executor(result1 => {
                otherAsyncValidation.then(result2 => resolve(result1.or(result2)), reject);
            }, reject);
        };
        return AsyncValidation(newExecutor);
    },
    getResult: () => {
        // to get the result let's use a good old js Promise
        return new Promise(executor).then(syncValidation => {
            // and call getResult on the resolved synchronous Validation
            return syncValidation.getResult();
        });
    },
});

It can be used like this:

const asyncValid = AsyncValidation((resolve, reject) => {
    resolve(Valid());
});

const asyncInvalid = AsyncValidation((resolve, reject) => {
    resolve(Invalid(['The value is invalid']));
});

asyncValid.and(asyncInvalid).getResult().then(result => // ['The value is invalid']);
asyncValid.and(asyncValid).getResult().then(result => // undefined);
asyncInvalid.and(asyncInvalid).getResult().then(result => // ['The value is invalid', 'The value is invalid']);

asyncValid.or(asyncInvalid).getResult().then(result => // undefined);
asyncValid.or(asyncValid).getResult().then(result => // undefined);
asyncInvalid.or(asyncInvalid).getResult().then(result => // ['The value is invalid', 'The value is invalid']);

With this Monoid, we can combine asynchronous validation, but how do we mix asynchronous validations with synchronous validations?

Async And Sync Together

First, we need to be able to convert synchronous Validations into asynchronous ones. Like so:

const AsyncValidation.of = (synchronousValidation) =>
    AsyncValidation((resolve) => resolve(synchronousValidation));

And to be able to tell if a Validation is Async we can test the presence of a then prop:

const isAsync = value => value.then;

Now, we can adapt AsyncValidation to accept synchronous validators:

const AsyncValidation = executor => ({
    then: executor,
    and: otherValidation =>
        AsyncValidation((resolve, reject) =>
            executor(result1 => {
                // cast otherValidation into an AsyncValidation if it is not already one
                const otherAsyncValidation = isAsync(otherValidation)
                    ? otherValidation
                    : AsyncValidation.of(otherValidation);
                otherAsyncValidation.then(
                    result2 => resolve(result1.and(result2)),
                    reject,
                );
            }, reject),
        ),
    or: otherValidation =>
        AsyncValidation((resolve, reject) =>
            executor(result1 => {
                // cast otherValidation into an AsyncValidation if it is not already one
                const otherAsyncValidation = isAsync(otherValidation)
                    ? otherValidation
                    : AsyncValidation.of(otherValidation);
                otherAsyncValidation.then(
                    result2 => resolve(result1.or(result2)),
                    reject,
                );
            }, reject),
        ),
    // map and getResult do not need to change
    map,
    getResult,
});

And similarly, we can adapt Valid and Invalid to accept AsyncValidation:

const Valid = () => ({
    x,
    isValid: true,
    and: otherValidation => {
        if (isAsync(otherValidation)) {
            // convert Valid into an asynchronous one and combine it with otherValidation
            return AsyncValidation.of(Valid()).and(otherValidation);
        }
        return otherValidation.isValid ? Valid() : other;
    },
    or: otherValidation => {
        if (isAsync(otherValidation)) {
            // convert the current Valid into an async one
            return AsyncValidation.of(Valid());
        }
        return Valid();
    },
});

const Invalid = x => ({
    x,
    isValid: true,
    and: otherValidation => {
        if (isAsync(otherValidation)) {
            // convert current Invalid into an asynchronous one
            // and combine it with otherValidation
            return AsyncValidation.of(Invalid(x)).and(otherValidation);
        }

        return otherValidation.isValid() ? Invalid(x) : Invalid(x.concat(otherValidation.x));
    },
    or: otherValidation => {
        if (isAsync(otherValidation)) {
            // convert current Invalid into an asynchronous one
            // and combine it with otherValidation
            return AsyncValidation.of(invalid(x)).or(otherValidation);
        }

        return otherValidation.isValid() ? Valid() : Invalid(x.concat(otherValidation.x));
    },
});

With this we can combine Valid, Invalid and AsyncValidation together like so:

const valid = Valid();
const invalid = Invalid(['The value is invalid'])

const asyncValid = AsyncValidation((resolve, reject) => {
    resolve(Valid());
});

const asyncInvalid = AsyncValidation((resolve, reject) => {
    resolve(Invalid(['The asynchronous value is invalid']));
});

asyncValid.and(valid).getResult().then(result => // undefined);
asyncValid.and(invalid).getResult().then(result => // ['The value is invalid']);


valid.and(asyncValid).getResult().then(result => // undefined);
valid.and(asyncInvalid).getResult().then(result => // ['The value is invalid']);

asyncInvalid.and(valid).getResult().then(result => // ['The asynchronous value is invalid']);
asyncInvalid.and(invalid).getResult().then(result => // ['The asynchronous value is invalid', 'The value is invalid']);

invalid.and(asyncValid).getResult().then(result => // ['The value is invalid']);
invalid.and(asyncInvalid).getResult().then(result => // ['The value is invalid', 'The value asynchronous is invalid']);

Now we have a Validation monoid comprised of the set of all Valid, Invalid, and AsyncValidation.

Finishing Touches

Now we need to adapt Validator (the Monoid of functions returning Validation) so that it also works with AsyncValidation, right? Well, actually no, there is nothing more to do. The Validator implementation does not need to change. After all, the Validation internals changed, but not its API.

Well there is one thing we can do: add a helper function to convert an async function to a Validator:

Validator.fromAsync = (asyncFunction) => {
    return Validator((value) => {
        return AsyncValidation((resolve, reject) => {
            asyncFunction(value).then(result => {
                if (result) {
                    resolve(Invalid(result);
                    return;
                }
                resolve(Valid());
            }).catch(reject);
        });
    });
};

With this we can do:

const doesUserIdExists = Validator.fromAsync(async (userId) => {
    const dbUser = await getUserFormDb(userIdd);
    return dbUser ? undefined : 'UserId does not exists in database';
});

const isNotDefined = Validator(value => {
    if (!value) {
        return Valid();
    }

    return Invalid(['userId can be empty']);
});

const isUserIdValid = doesUserIdExists.or(isNotDefined);

isNotDefined.check(null); // Promise(undefined);
isNotDefined.check('wrongId'); // Promise(['UserId does not exists in database', 'UserId can be empty']);
isNotDefined.check('goodId'); // Promise(undefined);

Bonus: LazyPromise Implementation

For fun, let's implement a fully functional LazyPromise.

This LazyPromise will be like a traditional promise, except that nothing will get executed until you call then.

const promise = LazyPromise(resolve =>
    setTimeout(() => resolve('hello'), 1000),
);

promise.then(result => {
    // here result is "hello"
});

This means that only then allows getting the result.

To modify the result, we will use map, which will allow applying a simple function over the resolved value.

promise
    .map(result => result.toUppercase())
    .then(result => {
        // here result is "HELLO"
    });

map will not function properly with functions returning another LazyPromise. With such functions, if we use map, we end up with a LazyPromise inside a LazyPromise.

promise
    .map(result =>  LazyPromise(resolve =>
        setTimeout(() => resolve(`${greeting} Lazyness`, 2000)),
    )
    .then(result => {
        // here result is a LazyPromise
        result.then(trueResult => {
            // Only here do we get the true result: "hello Lazyness"
        })

    });

For this case, we add chain to combine the two nested LazyPromise into one:

promise
    .chain(greeting =>
        LazyPromise(resolve =>
            setTimeout(() => resolve(`${greeting} Lazyness`, 2000)),
        ),
    )
    .then(result => {
        // here result is "hello Lazyness"
    });

Finally, we want to be able to catch errors. This means mapping a function onto a rejected value, and passing the result as the resolved value of a new LazyPromise.

LazyPromise((resolve, reject) => reject(new Error('Boom')))
    .catch(error => `Caught ${error.message}`)
    .then(result => {
        // here result is "Caught Boom"
    });

Ready? Go!

// executor is the same format as the argument of a promise
// (resolve: (result) => void, reject: (error) => void) => void;
// what LazyPromise does is allow to modify the then function without executing it
// To execute the async operation you just call the resulting `then` at the end
const LazyPromise = executor => ({
    then: executor,
    map: fn => {
        const newExecutor = (resolve, reject) => {
            executor(result => {
                // get the result of the original promise
                try {
                    // change it with the passed function
                    let modifiedResult = fn(result);
                    // then resolve with the updated result
                    resolve(modifiedResult);
                } catch (error) {
                    // if the mapped function threw an error
                    // reject the new promise with it
                    reject(error);
                }
            }
        }, reject);
        // create a new promise
        return LazyPromise(newExecutor);
    },
    // map function to the rejected error then pass the corrected error to resolve
    catch: fn => {
        const newExecutor = (resolve, reject) => {
            executor(resolve, error => {
                // get the error of the original promise
                try {
                    // change it with the passed function
                    let correctedError = fn(error);
                    // then resolve with the corrected error
                    resolve(correctedError);
                } catch (error) {
                    // if the mapped function threw an error
                    // reject the new promise with it
                    reject(error);
                }
            }
        });
        // create a new promise
        return LazyPromise(newExecutor);
    },
    // let's add a helper to transform a LazyPromize holding a LazyPromise into a simple LazyPromisea LazyPromise of a LazyPromise into a LazyPromise
    flatten: () => {
        const newExecutor = (resolve, reject) => {
            executor(otherLazyPromise => {
                // here `then` resolve to another LazyPromise
                // so let us removes it by resolving it
                otherLazyPromise.then(resolve, reject);
            }, reject);
        };
        return LazyPromise(newExecutor);
    },
    // Combining map and flatten, we can implement chain
    chain: (fn) => LazyPromise(executor).map(fn).flatten();
});

It can be used like this:

LazyPromise((resolve, reject) => {
    setTimeout(() => resolve(42), 2000);
})
.map(value => value + 5) // value is 42 + 5
.chain(value => LazyPromise((resolve, reject) => {
    // here value is 47
    reject('oh No!')
})).map(value => value + 7) // this will never be called
.catch(error => 0) // error is 'Oh No!'
.then((finalResult, error) => console.log({ finalResult, error })); // { finalResult: 0, error: undefined }

LazyPromise is actually a Monad. What this means, is that mathematicians have proven that this will always work as long as you follow the corresponding rules. Neat, right? If you are interested, here is an article I wrote on Monads

Think a moment about what it would take to implement the same with eager execution. Think of all the events to handles, the possible status of an eager promise... In fact, go take a look at BlueBird implementation of Promise.

Conclusion

Laziness is a powerful tool. By delaying execution, we can modify it without having to worry about state or race conditions. This can considerably simplify our code. So, plan ahead, and only execute the operations at the end, when everything is ready.

This concludes this series on checkoid internals. I hope this series has helped you to better understand and see the usefulness of functional programming.

Sure, there are some weird and intimidating terminology. But it can sure pack quite a punch!

Did you like this article? Share it!