Functional Validation in F# Using Applicatives
This is my entry for the F# Advent Calendar 2019 https://sergeytihon.com/tag/fsadvent/.
I'm using Railway-Oriented Programming (https://fsharpforfunandprofit.com/rop/) and was looking for an elegant, functional way to handle the conversion of an UnvalidatedType to a Result of ValidatedType or List of ValidationFailures (UnvalidatedUser -> Result< ValidatedUser, ValidationFailure list >). Having read Scott Wlaschin's excellent book (https://pragprog.com/book/swdddf/domain-modeling-made-functional), I knew that Applicatives were the functional answer to this problem.
This post is my interpretation of what I found.
Initial Solution
We start with a couple of simple record types and a discriminated union of potential validation failures:
open System open System.Text.RegularExpressions type UnvalidatedUser = { Name : string Email : string DateOfBirth : string } type ValidatedUser = { Name : string Email : string DateOfBirth : DateTime } type ValidationFailure = | NameIsInvalidFailure | EmailIsInvalidFailure | DateOfBirthIsInvalidFailure
We then add a number of partial active patterns and functions to handle validating the individual values:
let (|ParseRegex|_|) regex str = let m = Regex(regex).Match(str) if m.Success then Some (List.tail [ for x in m.Groups -> x.Value ]) else None let (|IsValidName|_|) input = if input <> String.Empty then Some () else None let (|IsValidEmail|_|) input = match input with | ParseRegex ".*?@(.*)" [ _ ] -> Some input | _ -> None let (|IsValidDate|_|) input = let (success, value) = DateTime.TryParse(input) if success then Some value else None let validateName input = // string -> Result<string, ValidationFailure list> match input with | IsValidName -> Ok input | _ -> Error [ NameIsInvalidFailure ] let validateEmail input = // string -> Result<string, ValidationFailure list> match input with | IsValidEmail email -> Ok email | _ -> Error [ EmailIsInvalidFailure ] let validateDateOfBirth input = // string -> Result <DateTime, ValidationFailure list> match input with | IsValidDate dob -> Ok dob //Add logic for DOB | _ -> Error [ DateOfBirthIsInvalidFailure ]
Note that the validate functions return a list of ValidationFailure in the Error case. This makes concatenating them together later on much easier.
We now add a simple helper function to create a ValidatedUser and the main validate function:
let create name email dateOfBirth = { Name = name; Email = email; DateOfBirth = dateOfBirth } let validate (input:UnvalidatedUser) : Result<ValidatedUser, ValidationFailure list> = let validatedName = input.Name |> validateName let validatedEmail = input.Email |> validateEmail let validatedDateOfBirth = input.DateOfBirth |> validateDateOfBirth // create validatedName validatedEmail validatedDateOfBirth
The last line is commented out because it obviously won't compile as the types don't match but it is an aspiration to end up with a solution that gets close to this.
Helper Functions
We need to create two functions; the first to handle the non-Result to Result mapping of 'create' and 'validatedName' and the second to handle the rest. The first is such a common requirement that it is built into F# Core (from 4.1) -> Result.map. It looks very similar to this:
let map f xResult = // ('a -> 'b) -> Result<'a,'c> -> Result<'b,'c> match xResult with | Ok x -> Ok (f x) | Error ex -> Error ex
This only works in this situation because of partial application and that is equally important for the Ok path in our next helper function:
let apply fResult xResult = // Result<('a -> 'b), 'c list> -> Result<'a,'c list> -> Result<'b,'c list> match fResult,xResult with | Ok f, Ok x -> Ok (f x) | Error ex, Ok _ -> Error ex | Ok _, Error ex -> Error ex | Error ex1, Error ex2 -> Error (List.concat [ex1; ex2])
The consequence of using partial application on the Ok track is that the partially applied function must always be the the first parameter to either function. This means that we can easily produce some working code to utilise these two helper functions that follow the rules to solve our requirement:
create |> Result.map <| validatedName |> apply <| validatedEmail |> apply <| validatedDateOfBirth
We can show that this works correctly by writing some example test functions:
let validTest = let actual = validate' { Name = "Ian"; Email = "hello@test.com"; DateOfBirth = "2000-02-03" } let expected = Ok { Name = "Ian"; Email = "hello@test.com"; DateOfBirth = DateTime(2000, 2, 3)} expected = actual let notValidTest = let actual = validate' { Name = ""; Email = "hello"; DateOfBirth = "" } let expected = Error [ NameIsInvalidFailure; EmailIsInvalidFailure; DateOfBirthIsInvalidFailure ] expected = actual
Whilst our function works, it contains back pipes <|
which Don Syme https://twitter.com/dsymetweets, the creator of F#, doesn't like, it is not considered the idiomatic way of writing applicatives in F# and doesn't really look anything like the result we aspire to achieve.
We can reduce some of the noise by removing all of the piping:
apply (apply (Result.map create validatedName) validatedEmail) validatedDateOfBirth
This is beginning to look like what we were originally looking to achieve. To progress further, we are going to use another of the really useful F# features; infix and prefix operators.
Infix/Prefix Operators
We have all seen the addition operator used as an infix:
let add x y = x + y
but it can also be used as a prefix:
let add x y = (+) x y
We can define our own operators:
let (<!>) = Result.map let (<*>) = apply
We can replace Result.map with (<!>)
and apply with (<*>)
in our code:
(<*>) ((<*>) ((<!>) create validatedName) validatedEmail) validatedDateOfBirth
If we now use the infix versions we get:
((create <!> validatedName) <*> validatedEmail) <*> validatedDateOfBirth
We then remove the unnecessary brackets and we finally end up with:
create <!> validatedName <*> validatedEmail <*> validatedDateOfBirth
You can easily verify that it works by running the two tests we created earlier.
If the number of elements get too large, we can rewrite it like this:
create <!> validatedName <*> validatedEmail <*> validatedDateOfBirth
I think you'll agree that this is an elegant solution to our original problem.
Putting It All Together
Let's finish off by showing the final codebase:
open System open System.Text.RegularExpressions type UnvalidatedUser = { Name : string Email : string DateOfBirth : string } type ValidatedUser = { Name : string Email : string DateOfBirth : DateTime } type ValidationFailure = | NameIsInvalidFailure | EmailIsInvalidFailure | DateOfBirthIsInvalidFailure let (|ParseRegex|_|) regex str = let m = Regex(regex).Match(str) if m.Success then Some (List.tail [ for x in m.Groups -> x.Value ]) else None let (|IsValidName|_|) input = if input <> String.Empty then Some () else None let (|IsValidEmail|_|) input = match input with | ParseRegex ".*?@(.*)" [ _ ] -> Some input | _ -> None let (|IsValidDate|_|) input = let (success, value) = DateTime.TryParse(input) if success then Some value else None let validateName input = // string -> Result<string, ValidationFailure list> match input with | IsValidName -> Ok input | _ -> Error [ NameIsInvalidFailure ] let validateEmail input = // string -> Result<string, ValidationFailure list> match input with | IsValidEmail email -> Ok email | _ -> Error [ EmailIsInvalidFailure ] let validateDateOfBirth input = // string -> Result<DateTime, ValidationFailure list> match input with | IsValidDate dob -> Ok dob //Add logic for DOB | _ -> Error [ DateOfBirthIsInvalidFailure ] let apply fResult xResult = // Result<('a -> 'b), 'c list> -> Result<'a,'c list> -> Result<'b,'c list> match fResult,xResult with | Ok f, Ok x -> Ok (f x) | Error ex, Ok _ -> Error ex | Ok _, Error ex -> Error ex | Error ex1, Error ex2 -> Error (List.concat [ex1; ex2]) let (<!>) = Result.map let (<*>) = apply let create name email dateOfBirth = { Name = name; Email = email; DateOfBirth = dateOfBirth } let validate (input:UnvalidatedUser) : Result<ValidatedUser, ValidationFailure list> = let validatedName = input.Name |> validateName let validatedEmail = input.Email |> validateEmail let validatedDateOfBirth = input.DateOfBirth |> validateDateOfBirth create <!> validatedName <*> validatedEmail <*> validatedDateOfBirth let validTest = let actual = validate' { Name = "Ian"; Email = "hello@test.com"; DateOfBirth = "2000-02-03" } let expected = Ok { Name = "Ian"; Email = "hello@test.com"; DateOfBirth = DateTime(2000, 2, 3)} expected = actual let notValidTest = let actual = validate' { Name = ""; Email = "hello"; DateOfBirth = "" } let expected = Error [ NameIsInvalidFailure; EmailIsInvalidFailure; DateOfBirthIsInvalidFailure ] expected = actual
I love functional programming with F#. Nothing makes me happier than finding simple, elegant solutions to interesting problems.
Further Reading
If you want a deeper dive into the wonders of applicatives and a lot more, I highly recommend that you read the following:
https://fsharpforfunandprofit.com/posts/elevated-world-3/#validation
https://blog.ploeh.dk/2018/10/01/applicative-functors/
Finally
Thanks to Sergey Tihon for https://sergeytihon.com/tag/newsf-weekly/ and for running the F# Advent Calendar, Scott Wlaschin for writing https://pragprog.com/book/swdddf/domain-modeling-made-functional and making https://fsharpforfunandprofit.com/ such an amazing resource plus special thanks to all of you in the F# community for being so awesome. :)