Behold the Power of Laziness With Functional Programming
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.
- Part 1: The True Power Of Monoids: How To Combine Validators
- Part 2: Validator assemble! Functor in action.
- Part 3: Behold the Power of Laziness With Functional Programming
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!