神刀安全网

Practical Intro to Monads in JavaScript: Validation

Few weeks ago I published a practical Intro to Monads in JavaScript where I covered basics of Identity and Maybe monads. Some time later I added a tutorial on Either monad and fails-fast error handling . This time I will show you error accumulation in a simple Validation use case.

Well, it may be all right in practice, but it will never work in theory.

Warren Buffett

All examples are based on monet.jsa tool bag that assists Functional Programming by providing a rich set of Monads and other useful functions. I’ll use arrow functions introduced in ES6. They are much more readable than regular functions when used as single operation “callbacks” . Also in most examples TypeScript type definitions will be added to enhance overall readability.

Either Validation success or Validation fail

Do you remember the Either monad ? The one that could have been either Right(value) or Left(error) ? The Validation also has two sides. It can hold Success(value) or Fail(error) . As monet.js Validation docs state:

Validation is not quite a monad as it doesn’t quite follow the monad rules , even though it has the monad methods.

Validation is similar to a Monad, and the difference is academic. Since we’re only interested in practice, not theory, let’s make Validation the next step in our monadic adventures in JavaScript.

Validation in practice

Validation – this name is quite straightforward, isn’t it? Probably we should validate something! As we already know from intro of this article, it’s capable of accumulating errors (failures). It’s for sure a good tool to validate a collection of things that may succeed or fail to meet some rules. Or one item that has to be validated against many rules. The HTML <input /> element would be a great testing ground!

Let’s say we have some validator, from a 3rd party library, that takes an input value and a validation type label and it returns true / false :

function validate(value: any, type: string): boolean; 

Now we can wrap it to work with Validation instead of boolean and add somecurrying:

function isValid<V>(type: string) {     return (value: V): Validation<string, V> =>          validate(value, type) ?             Success(value) :             Fail(type); } 

Now we have our implementation of a validator which returns a Validation that can hold a string with info about the failure or a successfully validated value of input. Let’s make it work together:

// Assuming we have an `input` value of type `HTMLInputElement` const validated = isValid('REQUIRED')(input.value)     .flatMap(isValid('EMAIL'))     .flatMap(isValid('LEGAL_DOMAINS')); // …? 

Something is wrong here, isn’t it? We achieved fast-fail – if any isValid() call returned fail, we go straight to end of chain with a failed Validation . That is not what we wanted – we do not know if any other validation has failed.

Error accumulation

Fortunately monet.js Validation has an .ap() method which implements the applicative functor pattern and provides a way to accumulate errors:

interface Validation<E, V> {     …     ap<Z>(v: Validation<E, (V) => Z>): Validation<E, Z>;     … } 

So what? So, we can collect our error information! The .ap() method accepts a Validation of a function and:

– if both validations are successful it binds this function to held value and wraps returned value (just like .map() ) in a successful Validation ;

– if one of these validations is failure it just returns the failed Validation ;

– if both validations are failures it returns a Validation holding accumulated errors.

We’ll need some kind of value accumulator. Actually there is one provided by monet.js – the [ .acc() method] on a Validation. It returns a successful Validation holding a magic function – a function returning itself. For ever.

type ValidationAcc = () => ValidationAcc;  interface Validation<E, V> {     …     ap<Z>(v: Validation<E, (V) => Z>): Validation<E, Z>;     acc(): Validation<E, ValidationAcc>;     … } 

Then we can:

const validated = isValid('EMAIL')(input.value)     .ap(isValid('LEGAL_DOMAINS')(input.value)         .ap(isValid('REQUIRED')(input.value).acc())); 

The validated would reference either a successful Validation holding that magic accumulator function or a failed one with accumulated errors inside. We will just need to .map() successful output to a Validation holding the input.value :

const validated = isValid('EMAIL')(input.value)     .ap(isValid('LEGAL_DOMAINS')(input.value)         .ap(isValid('REQUIRED')(input.value).acc()))     .map(() => input.value); 

We get a success with input value or… concatenated strings, e.g. 'LEGAL_DOMAINSREQUIRED' . Not so good…

Clean it up

Implementation of a Validation .ap() method accumulates failure information using Semigroup concatenation . If it gets a string, it just adds previous one to current. Otherwise it checks for .concat() method on passed failure. If no .concat() method is available it throws. It seems that we have to wrap our possible failures in an array, right?

const validated = isValid('EMAIL')(input.value).failMap(Array)     .ap(isValid('LEGAL_DOMAINS')(input.value).failMap(Array)         .ap(isValid('REQUIRED')(input.value).failMap(Array).acc()))     .map(() => input.value); 

What a spaghetti… To make it a bit more cleaner let’s move some parts to the inside of isValid and switch the args order:

function validateValue<V>(value: V) {      return (type: string): Validation<Array<string>, V> =>          validate(value, type) ? Success(value) : Fail([type]); } 

Then we can:

const isValid = validateValue(input.value);  const validated = isValid('EMAIL')     .ap(isValid('LEGAL_DOMAINS')         .ap(isValid('REQUIRED').acc()))     .map(() => input.value); 

And we get a success with input value or an array of strings representing failed validations. It has only one problem – we’ll have to nest ap in ap in apn times, where n is number of validations made on single value. It would be much better to put this computation into a loop:

const isValid = validateValue(input.value);  const validated = ['EMAIL', 'LEGAL_DOMAINS', 'REQUIRED']     .map(isValid)     .reduce((acc, validation) => validation.ap(acc), Success().acc())     .map(() => input.value); 

The example above does exactly the same as the one before, but it can accept any number of validation rules. The only downside is the Success().acc() passed as accumulator to the .reduce() . And we can actually move it inside .reduce() loop, because if no acc is passed, it’ll take first two items on first run (instead of accumulator and first item). This way:

const isValid = validateValue(input.value);  const validated = ['EMAIL', 'LEGAL_DOMAINS', 'REQUIRED']     .map(isValid)     .reduce((acc, validation) => validation.ap(acc.acc()))     .map(() => input.value); 

Now we can just wrap it in a function now…

Finally

…so here is a final, reusable tool:

function validate(value, type: string): boolean;  function validateValue<V>(value: V) {      return (type: string): Validation<Array<string>, V> =>          validate(value, type) ? Success(value) : Fail([type]); }  function validateInput<T>(value: T, ...validationRules: string[]): Validation<string[], T> {     const isValid = validateValue(value);     return validationRules.map(isValid)         .reduce((acc, validation) => validation.ap(acc.acc()))         .map(() => value); } 

Is that all?

No ��

This was an example of error accumulation for testing a single value. There is a different pattern for accumulating many values – e.g. when validating a whole form. But to achieve this we’ll have to use an immutable List which I’ll introduce in next article.

Further reading

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Practical Intro to Monads in JavaScript: Validation

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址