Grokking Applicative Validation

choc13

Matt Thornton

Posted on April 16, 2021

Grokking Applicative Validation

Previously in Grokking Applicatives we discovered Applicatives and more specifically invented the apply function. We did this by considering the example of validating the fields of a credit card. The apply function allowed us to easily combine the results we obtained from validating the number, expiry and CVV individually into a Result<CreditCard> that represented the validation status of an entire CreditCard. You might also remember we somewhat glossed over the error handling when multiple fields were invalid. We took the easy road and just returned the first error that occurred.

An unhappy customer 😡

In the spirit of agile we decided to ship our previous implementation, because, well it was better than nothing. A short while later, customers start complaining. All the complaints are along these lines.

"I entered my credit card details on your site and it took three attempts before it was finally accepted. I submitted the form and each time it gave me a new error. Why couldn't you tell me about all the errors at once?"

To see this more clearly consider a customer that enters the following data, in JSON form.

{
    “number”: “a bad number”,
    “expiry”: “invalid expiry”,
    “cvv”: “not a CVV”
}
Enter fullscreen mode Exit fullscreen mode

The first time they submit the form they get an error like ”’a bad number’ is not a valid credit card number”. So they fix that and resubmit. Then they get a message like ”’invalid expiry’ is not a valid expiry date”. So they fix that and submit a third time and still receive an error along the lines of ”’not a CVV’ is not a valid CVV”. Pretty annoying!

We should be able to do better and return all of the errors at once. We even previously pointed out that all of the field level validation functions were independent of each other. So there was no good reason not to run all of the functions and aggregate the errors if there were any, we were just being lazy!

A better validation Applicative đŸ’Ș

Let's start by updating the signature of validateCreditCard to signify our new desire to return all the validation errors that we find.

let validateCreditCard (card: CreditCard): Result<CreditCard, string list>
Enter fullscreen mode Exit fullscreen mode

The only change here is that we’re now returning a list of error messages rather than a single one. How should we update our implementation to satisfy this new signature?

Let’s return to the apply function that we defined before and see if we can just fix it there. It would be very nice if all we had to do was modify apply and leave validateCreditCard otherwise unchanged.

For reference here’s the apply function that we wrote last time, the one that returns the first error it encounters.

let apply a f =
    match f, a with
    | Ok g, Ok x -> g x |> Ok
    | Error e, Ok _ -> e |> Error
    | Ok _, Error e -> e |> Error
    | Error e1, Error _ -> e1 |> Error
Enter fullscreen mode Exit fullscreen mode

We can see from this that it’s only the final case where we have multiple errors to deal with and so it’s only there that we need to fix things. The simplest fix is to just concatenate both errors. This has the effect of building up a list of errors each time we call apply with invalid data. Let’s see what that looks like then.

let apply a f =
    match f, a with
    | Ok g, Ok x -> g x |> Ok
    | Error e, Ok _ -> e |> Error
    | Ok _, Error e -> e |> Error
    | Error e1, Error e2 -> (e1 @ e2) |> Error
Enter fullscreen mode Exit fullscreen mode

That was easy, we just used @ to concatenate the two lists in the case where both sides were Error. Everything else remained the same.

Let’s walk through validating the credit card step-by-step with the example of the bad data that the customer was supplying earlier. First we call Ok (createCreditCard) |> apply (validateNumber card.Number). This hits the third case of the pattern match in apply because f is Ok, but the argument a is Error. That returns us something like an Error [ “Invalid number” ], but whose type is still Result<string -> string -> CreditCard, string list>.

We then pipe this like |> apply (validateExpiry card.Expiry). This hits the final case in the pattern match because now both f and a are Error. This means the @ operator is used to concat the errors together to create something like Error [ “Invalid expiry”; “Invalid number” ]. The type of which is now Result<string -> CreditCard, string list> because we now just need to supply a CVV to finish creating the CreditCard.

So in the final step we do exactly that and pipe this result like |> apply (validateCvv card.Cvv). Just like the last step we hit the case where both f and a are Error and so we concat them. Now we’ve got something with the type Result<CreditCard, string list> as we wanted with a value like Error [ “Invalid CVV”; “Invalid expiry”; “Invalid number” ].

A small compile time error

You might have spotted that we’ve actually changed the type of the apply function now. By using the @ operator F# has inferred that the errors must be a list. So now the signature of apply is Result<T, E list> -> Result<T -> V, E list> -> Result<V, E list>.

We now have an apply that works for Result<T, E list>. That is, it works for any results where the errors are contained in a list, rather than being single values like a string. There are a couple of interesting points to make about this:

  1. The errors in the list can be any type, providing they’re all of the same type.
  2. All of our validated results must now have a list of errors if we want to use them with apply.

Point 1 is useful because it allows us to model our errors in more meaningful ways than just using strings. Although for the rest of this post we’ll keep using string in order to keep it simple. Modelling errors deserves a blog post of its own.

Point 2 however causes us a little problem we have to solve here. Our original field level validation functions are still returning Result<string, string> so they no longer work with our new version of apply.

We have two choices when it comes to fixing this issue. We could keep the functions as they are and transform their outputs by wrapping the error, if it exists, of the result in a list. Which might look something like this.

let validateCreditCard (card: CreditCard): Result<CreditCard, string list> =
    let liftError result =
        match result with
        | Ok x -> Ok x
        | Error e -> Error [ e ]
    Ok (createCreditCard)
    |> apply (card.Number |> validateNumber |> liftError)
    |> apply (card.Expiry |> validateExpiry |> liftError)
    |> apply (card.Cvv |> validateCvv |> liftError)
Enter fullscreen mode Exit fullscreen mode

The other choice is to update those field validation functions so that they return Result<string, string list> as required. It might be tempting to take the first choice and if we had no control over those functions we’d have to do that. However, by letting those field level functions return a list we give them the flexibility to do more complex validation and potentially indicate multiple errors.

For instance the validateNumber function could indicate both a problem with the length and the presence of invalid characters like this.

let validateNumber number: Result<string, string list> =
    let errors = 
        if String.length num > 16 then
            [ "Too long" ]
        else
            []
    let errors = 
        if num |> Seq.forall Char.IsDigit then
            errors
        else 
            "Invalid characters" :: errors

    if errors |> Seq.isEmpty then
        Ok num
    else
        Error errors
Enter fullscreen mode Exit fullscreen mode

Using Result<T, E list> throughout gives us a more composable and flexible api that allows us to refactor the errors returned from those functions in the future without affecting the rest of the program.

So given that they’re functions in our domain, then we’ll take that approach. Let’s give that a try and see what it looks like all together when using this new version of apply.

let validateNumber num: Result<string, string list> =
    if String.length num > 16 then
        Error [ “Too long” ]
    else
        Ok num

let validateExpiry expiry: Result<string, string list> =
    // validate expiry and return all errors we find

let validateCvv cvv: Result<string, string list> =
    // validate cvv and return all cvv errors we find

let validateCreditCard (card: CreditCard): Result<CreditCard, string list> =
    Ok (createCreditCard)
    |> apply (validateNumber card.Number)
    |> apply (validateExpiry card.Expiry)
    |> apply (validateCvv card.Cvv)
Enter fullscreen mode Exit fullscreen mode

Lovely job! Apart from a couple of small changes to lift the errors up into lists within validateNumber etc the rest has stayed the same. In particular, the body of validateCreditCard is completely unchanged.

Do I have to use list for the errors

The only requirement we’ve placed on the error type is that we can use the @ operator to concat the errors together. So as long as the errors are concat-able then we can use a different type here. The fancy category theory name for this is a semi-group. A semi-group is anything that has at least a concat operator defined for it. A common type to use here is a NonEmptyList, because we know that if the result is an Error then they’ll be at least one item in the list.

A tale of two applicatives 📗

We’ve seen two implementations of apply for Result now. Can we have both? Unfortunately not really, at least not both defined in the Result module of F#. In order to do this F# would have to be able to decide which one to use based on whether the error type supported concat, which might not even be obvious without an explicit type annotation. Even then we might get undesired results because strings support concat, but it’s unlikely we want to concat the individual error messages into one long string.

How should we decide which one is correct then? Well, we don’t have to. We can define another type called Validation which has a Success case and a Failure case, similar to Ok and Error for Result. The difference is that for Validation we can define apply using the version we’ve created in this post which accumulates errors and for Result use the apply function that short circuits and returns the first error, that we saw in the last post. Luckily for us the excellent FSharpPlus library has already done exactly that.

What did we learn đŸ§‘â€đŸ«

We’ve seen that applicatives are a great tool to have at our disposal when writing validation code. They allow us to write validation functions for each field and then easily compose these to create functions for validating larger structures made up of those fields.

We’ve also seen that whilst applicative computations are usually independent of each other there’s nothing to guarantee that a particular implementation of apply will make full use of this. Specifically, when working with validation we want to make sure that apply accumulates all errors and so we should make sure to use a type like Validation from FSharpPlus to get this behaviour.

💖 đŸ’Ș 🙅 đŸš©
choc13
Matt Thornton

Posted on April 16, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Grokking Lenses
fsharp Grokking Lenses

May 28, 2021

Interpreting Free Monads
fsharp Interpreting Free Monads

May 21, 2021

Grokking Free Monads
fsharp Grokking Free Monads

May 14, 2021

Grokking Monad Transformers
fsharp Grokking Monad Transformers

May 7, 2021

Grokking the Reader Monad
fsharp Grokking the Reader Monad

May 1, 2021