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. :)

https://twitter.com/ijrussell

Zurück
Zurück

ADRs as a Tool to Build Empowered Teams

Weiter
Weiter

Announcing Domain-Driven Design Exercises