Validator assemble! Functors in Action
In a previous article, I introduced Checkoid a library where you combine validators like lego pieces. This article is the second part in a series of three that will use the Checkoid internals as an example to illustrate several functional techniques.
- Part 1: The True Power Of Monoids: How To Combine Validators
- Part 2: Validator assemble! Functors in Action (this article)
- Part 3: The almighty power of laziness: How to concat async validator.
In this new functional programming tutorial, I'll explain how Functors can help us assemble validator functions to validate complex data structures like objects.
Reminder: Reducing Function Arguments
In a previous article about functional programming, I explained how the concat
operation allows to reduce an array of monoids into a single monoid, provided we have a neutral monoid to initialize the reduce call:
monoids.reduce(concat, neutral) => one monoid
For instance, for the monoid composed of the boolean set (values are true
or false
) and the and
operation as concat
, we can reduce an array of monoids as follows:
const and = (a, b) => a && b;
const neutral = true; // x && true will always return x
[true, false, true].reduce(and, neutral);
// neutral && true && false && true => false
Also, in the previous article, I introduced the "Validator" monoid, which allows to validate inputs:
const isGreaterThan = max =>
validator(value => {
if (value > max) {
return;
}
return `value must be greater than ${max}`;
});
const isSmallerThan = min =>
validator(value => {
if (value < min) {
return;
}
return `value must be less than ${min}`;
});
const isBetweenThreeAndTen = isGreaterThan(3).and(isSmallerThan(10));
isBetweenThreeAndTen.check(11); // ['value must be less than 10']
isBetweenThreeAndTen.check(2); // ['value must be greater than 3']
isBetweenThreeAndTen.check(5); // undefined
So we can use the reduce technique to transform an array of Validators into a single Validator. But why would we want to do that?
The Object Validator
What I want to achieve is to validate the properties of an object, like so:
// this person is valid, because
{
firstName: 'John', // should not be empty
lastName: 'Doe', // should not be empty
age: 35, // should be over 18
}
I already know how to write Validators for individual properties, what I need is a way to combine them into a single validator function for validating an entire object. Sounds familiar? Yes, we'll use the reduce technique.
What we want is to take what I call a specification: an object describing what we want by pairing key with validator :
{
firstName: isNotNull, // should not be empty
lastName: isNotNull, // should not be empty
age: isGreaterThan(18), // should be over 18
}
And transform it into a single Validator.
Here goes a first implementation:
// first of all, we need the neutral element on and
const neutral = Validator(value => {
return Validation.Valid(value);
});
const objectValidator = spec => {
// first create an array of validators for each key
const validators = Object.keys(spec)
.map(key => {
const fieldValidator = value => {
const testedValue = value && value[key];
return spec[key].run(testedValue);
};
return Validator(fieldValidator);
});
// then concat them with `and` using reduce and `isObject` as starting point
return validators.reduce((acc, validator) => acc.and(validator), neutral);
}
It's already functional:
const personValidator = objectValidator({
firstName: isNotNull,
lastName: isNotNull,
age: isGreaterThan(18).or(isNull);
});
personValidator.check({ firstName: 'John', lastName: 'Doe' });
// age is undefined, so the input is valid
personValidator.check('foo');
// [
// 'firstName must not be null',
// 'lastName must not be null'
// ]
personValidator.check({ firstName: 'John', lastName: 'Doe', age: 12 })
// [
// 'value must be greater than 18',
// ]
We Need Context, Functors To The Rescue
Ok, so it works, but the error messages are in an array, and I would like to know which message corresponds to which key. How can I do that?
We could change the validator run
function so that it takes a key, but then we'd have to test if it received a key. And clearly, the validator run
function should not know where it's used according to the law of demeter.
No, what we need is a way for object validators to edit the error messages they output to transform them into messages with a key.
But where are those messages exactly? We have a validator that holds a run
function that returns a Validation
when executed. And this Validation
holds the list of all errors if it is invalid. That's what we want to edit.
So first need to update the Validation
type, so that it offers a map
method to map function on its message. As a reminder, the Validation
type that I defined in the first article is composed of the Valid
and Invalid
subtypes:
const Validation = { Valid, Invalid };
// Valid has no message, so this map method does nothing
const Valid = () => ({
...,
map: fn => Valid(), // it's valid, so no message to change
});
const Invalid = invalidResults => ({
...,
map: fn => {
// takes the function and applies it to all message in invalidResults
const newInvalidResults = invalidResults.map(fn);
// put the result back into invalid
return Invalid(newInvalidResults);
},
});
Now that we can map a function on the Validation
messages, we can add a function to the Validator
monoid that will call the Validation.map
function for us.
But wait, how can Validator call the map method of the Validation. It will only be created when the check method gets called.
Impossible! We cannot edit something that does not exist yet. Except... Except we're using functional programming. So, instead of trying to change a value that does not exist, we can change the function that creates the value.
That's easier said than done. We have to call the Validation.map
function inside the Validator
since it is the result of the function called run
. But we do not want to execute it yet...
Let's abstract from the Validator
object for a moment. We have a validator function: a function that takes a value, tests it, and then either returns a Valid
or an Invalid
based on the result of the test. This is how it the validtor function looks when we want to test that the value is a string:
const isString = (value) => {
if (typeof value === 'string') {
return Valid();
}
return Invalid('Value must be a string');
};
How can we call map
on the result of this run function without executing it? We wrap it unside another function, like this:
const modifiedIsString = value => {
const validation = isString(value);
return validation.map(message => {
// here we can modify the validation result
});
}
And since it's a common needd, I want to modifiy other functions the same way, so I built a helper function to do it. It's called format
:
const format = (run, fn) => {
// We create a new run function derived from the original run
const modifiedRun = value => {
// In this function we execute the original run function, which gives us the Validation
const validation = run(value);
// then we can map the function on it
return validation.map(fn);
};
return modifiedRun;
}
This is how I can use it:
const isStringWithKeyName = format(isString, result => ({ ...result, key: 'name' }));
isStringWithKeyName('hello'); // undefined
isStringWithKeyName(42); // [{ message: 'Value must be a string', key: 'name' }]
Now, let's go back to the Validator
object. I'll bind the format
function to it, by using a slightly modified version since Validator
already takes run
as a parameter:
const Validator = run => ({
...,
// add a format function to pass a function to Validation.map
format: fn => {
// We modify run using our format helper
const runWithFormat = format(run, fn);
// And put the modified run into a new validator
return Validator(runWithFormat);
},
});
Finally, let's use the new format
function in objectValidator
to transform an error message into an object containing both the tested key and the error message:
const objectValidator = spec =>
Object.keys()
.map(key => {
const validationFunction = spec[key].run;
const validationFunctionOnKey = (value) => {
const testedValue = value && value[key];
return validationFunction(testedValue);
}
- return Validator(validationFunctionOnKey);
+ return Validator(validationFunctionOnKey)
+ .format(message => ({ message, key });
})
.reduce((acc, validator) => acc.and(validator), neutral);
As a matter of fact, I turned the Validator
and Validation
Monoids into Functors. The format
method is equivalent to the map
method of a functor. But in this case, format
is a more descriptive name.
With format
, we pass a function to the validator and it knows how to eventually apply it to the targeted value. It does not matter that this value does not exist yet.
Because when the value arrives, it will be modified by the given function.
Let's see if it works:
const personValidator = objectValidator({
firstName: isString,
lastName: isString,
age: isGreaterThan(18).or(isNull);
});
personValidator.check({ firstName: 'John', lastName: 'Doe' });
// age is undefined, so the input is valid
personValidator.check({ firstName: 42, lastName: 13 });
// [
// { key: 'firstName', message: 'value must be a string' },
// { key: 'lastName', message: 'value must be a string' }
// ]
personValidator.check({ firstName: 'John', lastName: 'Doe', age: 12 })
// [
// { key: 'age', message: 'value must be greater than 18' },
// ]
Additionally, as format
allow us to change the result as we want, we can use it to customize validator results like this:
personValidator
.format(({ key, message }) => `person.${key} ${message}`);
.check({ firstName: 42, lastName: 13 });
// [
// 'person.firstName value must be a string',
// 'person.lastName value must be a string'
// ]
Conclusion
We saw that with monoids, you can combine a lot of validators. And we saw that with functors, it becomes possible to apply functions to values even before they exist, like a time traveling-wizard.
And here you have it, the basics to have a simple validation library like Checkoid.
Working on functions allowed us to add functionality that would appear like magic for those who are used to work with objects. Imagine doing something like this with object-oriented paradigm? Seems hard, right?
Next time, we will see how we can also support asynchronous validation. We'll also uncover the biggest default of JavaScript Promises.