Validator assemble! Functors in Action

Thiery Michel
Thiery MichelFebruary 12, 2021
#functional-programming#js

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.

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.

Did you like this article? Share it!