loading...

Getting started with fp-ts: Either vs Validation

gcanti profile image Giulio Canti Updated on ・3 min read

The problem

Say you must implement a web form to signup for an account. The form contains two field: username and password and the following validation rules must hold:

  • username must not be empty
  • username can't have dashes in it
  • password needs to have at least 6 characters
  • password needs to have at least one capital letter
  • password needs to have at least one number

Either

The Either<E, A> type represents a computation that might fail with an error of type E or succeed with a value of type A, so is a good candidate for implementing our validation rules.

For example let's encode each password rule

import { Either, left, right } from 'fp-ts/lib/Either'

const minLength = (s: string): Either<string, string> =>
  s.length >= 6 ? right(s) : left('at least 6 characters')

const oneCapital = (s: string): Either<string, string> =>
  /[A-Z]/g.test(s) ? right(s) : left('at least one capital letter')

const oneNumber = (s: string): Either<string, string> =>
  /[0-9]/g.test(s) ? right(s) : left('at least one number')

We can chain all the rules using... chain

import { chain } from 'fp-ts/lib/Either'
import { pipe } from 'fp-ts/lib/pipeable'

const validatePassword = (s: string): Either<string, string> =>
  pipe(
    minLength(s),
    chain(oneCapital),
    chain(oneNumber)
  )

Because we are using Either the checks are fail-fast. That is, any failed check shortcircuits subsequent checks so we will only ever get one error.

console.log(validatePassword('ab'))
// => left("at least 6 characters")

console.log(validatePassword('abcdef'))
// => left("at least one capital letter")

console.log(validatePassword('Abcdef'))
// => left("at least one number")

However this could lead to a bad UX, it would be nice to have all of these errors be reported simultaneously.

The Validation abstraction may help here.

Validation

Validations are much like Either<E, A>, they represent a computation that might fail with an error of type E or succeed with a value of type A, but as opposed to the usual computations involving Either, they are able to collect multiple failures.

In order to do that we must tell validations how to combine two values of type E.

That's what Semigroup is all about: combining two values of the same type.

For example we can pack the errors into a non empty array.

The 'fp-ts/lib/Either' module provides a getValidation function that, given a semigroup, returns an alternative Applicative instance for Either

import { getSemigroup } from 'fp-ts/lib/NonEmptyArray'
import { getValidation } from 'fp-ts/lib/Either'

const applicativeValidation = getValidation(getSemigroup<string>())

However in order to use applicativeValidation we must first redefine all the rules so that they return a value of type Either<NonEmptyArray<string>, string>.

Instead of rewriting all the previous functions, which is cumbersome, let's define a combinator that converts a check outputting an Either<E, A> into a check outputting a Either<NonEmptyArray<E>, A>

import { NonEmptyArray } from 'fp-ts/lib/NonEmptyArray'
import { mapLeft } from 'fp-ts/lib/Either'

function lift<E, A>(check: (a: A) => Either<E, A>): (a: A) => Either<NonEmptyArray<E>, A> {
  return a =>
    pipe(
      check(a),
      mapLeft(a => [a])
    )
}

const minLengthV = lift(minLength)
const oneCapitalV = lift(oneCapital)
const oneNumberV = lift(oneNumber)

Let's put all together, I'm going to use the sequenceT helper which takes n actions and does them from left-to-right, returning the resulting tuple

import { sequenceT } from 'fp-ts/lib/Apply'
import { map } from 'fp-ts/lib/Either'

function validatePassword(s: string): Either<NonEmptyArray<string>, string> {
  return pipe(
    sequenceT(getValidation(getSemigroup<string>()))(
      minLengthV(s),
      oneCapitalV(s),
      oneNumberV(s)
    ),
    map(() => s)
  )
}
console.log(validatePassword('ab'))
// => left(["at least 6 characters", "at least one capital letter", "at least one number"])

Appendix

Note that the sequenceT helper is able to handle actions with different types:

interface Person {
  name: string
  age: number
}

// Person constructor
const toPerson = ([name, age]: [string, number]): Person => ({
  name,
  age
})

const validateName = (s: string): Either<NonEmptyArray<string>, string> =>
  s.length === 0 ? left(['Invalid name']) : right(s)

const validateAge = (s: string): Either<NonEmptyArray<string>, number> =>
  isNaN(+s) ? left(['Invalid age']) : right(+s)

function validatePerson(name: string, age: string): Either<NonEmptyArray<string>, Person> {
  return pipe(
    sequenceT(applicativeValidation)(validateName(name), validateAge(age)),
    map(toPerson)
  )
}

Posted on by:

Discussion

markdown guide
 

Hi Giulio, thanks for the great library and the great documentation. I'm following along, and I'd like to know how would you implement a function that takes a list of Validators and applies them to a single input, something along these lines:

import { Either } from "fp-ts/lib/Either";
import { NonEmptyArray } from "fp-ts/lib/NonEmptyArray";

type Validation<E, A> = Either<NonEmptyArray<E>, A>;
type Validator<E, A> = (a: A) => Validation<E, A>;

declare function validations<E, A>(vs: Validator<E, A>[], a: A): Validation<E, A>;

Notice that for simplicity I'm making the Validation type take a NonEmptyArray of errors, since I'm mainly interested on how to combine the validations from a dynamically constructed array.

Finally, if the list of validations is empty, we can assume the input is valid.

Thanks!

 

This signature is weird

type Validator<E, A> = (a: A) => Validation<E, A>;

If you already know that the input has type A, why are you validating in the first place?

Check this out: Parse, don't validate

 

Ah, yes. That's a very good observation thanks. Most likely the Validator would be producing some "valid" version of A. Point taken.

Now, if we come back to the original email address validation example, how would you combine a dynamically built list of email validation functions? Suppose we load the validations to the client from a service, that gives us the business rules we want to validate?

The current example takes a NonEmptyArray as imput, and I have not been able to find a way to combine validations from an Array.

Again, many thanks, and thanks for your suggestion.

 

Thank you so much for those articles, Fp-ts has done a lot to increase my speed everyday at work!

I'm struggling to figure what this would look like without the sequenceT helper. Would you have time to provide a snippet ?

(I tried going through the sequenceT source but I can't quite figure out what's going on yet)

 

This is what sequenceT is doing under the hood (when specialized to getValidation(getSemigroup<string>()) + three validations)

function specializedSequenceT(
  firstValidation: Either<NonEmptyArray<string>, string>,
  secondValidation: Either<NonEmptyArray<string>, string>,
  thirdValidation: Either<NonEmptyArray<string>, string>
): Either<NonEmptyArray<string>, [string, string, string]> {
  // Applicative instance for `Either<NonEmptyArray<string>, A>`
  const V = getValidation(getSemigroup<string>())

  // builds a tuple from three strings
  const tuple = (a: string) => (b: string) => (c: string): [string, string, string] => [a, b, c]

  // manual lifting, check out the "Lifting" section in "Getting started with fp-ts: Applicative"
  return V.ap(V.ap(V.map(firstValidation, tuple), secondValidation), thirdValidation)
}

function validatePassword(s: string): Either<NonEmptyArray<string>, string> {
  return pipe(
    specializedSequenceT(minLengthV(s), oneCapitalV(s), oneNumberV(s)),
    map(() => s)
  )
}
 

Ohhhh, okay, the tuple of results is what you apply to. Thank you so much for your answer!
(And yes, going back to the applicative article right now 😉)

 

How would this look in 2.0.0-rc.7?
For starters getArraySemigroup and getApplicative seems to be missing :)

 

With fp-ts@2 will be something like

import { sequenceT } from 'fp-ts/lib/Apply'
import { Either, getValidation, left, map, mapLeft, right } from 'fp-ts/lib/Either'
import { getSemigroup, NonEmptyArray } from 'fp-ts/lib/NonEmptyArray'
import { pipe } from 'fp-ts/lib/pipeable'

const minLength = (s: string): Either<string, string> =>
  s.length >= 6 ? right(s) : left('at least 6 characters')

const oneCapital = (s: string): Either<string, string> =>
  /[A-Z]/g.test(s) ? right(s) : left('at least one capital letter')

const oneNumber = (s: string): Either<string, string> =>
  /[0-9]/g.test(s) ? right(s) : left('at least one number')

function lift<L, A>(
  check: (a: A) => Either<L, A>
): (a: A) => Either<NonEmptyArray<L>, A> {
  return a =>
    pipe(
      check(a),
      mapLeft(a => [a])
    )
}

function validatePassword(s: string): Either<NonEmptyArray<string>, string> {
  return pipe(
    sequenceT(getValidation(getSemigroup<string>()))(
      lift(minLength)(s),
      lift(oneCapital)(s),
      lift(oneNumber)(s)
    ),
    map(() => s)
  )
}

console.log(validatePassword('ab'))
/*
=> left([ 'at least 6 characters',
    'at least one capital letter',
    'at least one number' ])
*/
 

I've been following your blog series, and am keen to better understand how this applicative validation works so I pasted your sample above into my code. Unfortunately I get "Argument of type 'Either, [string, string, string]>' is not assignable to parameter of type 'unknown[]'." error on the sequenceT(getValidation...line above. Using fp-ts @ 2.2.0 and Typescript @ 3.7.3. What would be expecting a parameter of type unknown[]?

Sorry, I can't repro, the sample above looks fine

 

Thats gr8 !
function lift(check: (a: A) => Either): (a: A) => Either, A> {

 

how to extract the left validation messages or right person object from the validatePerson method