Understanding F# applicatives and custom operators
[This post originally appeared on Jonathan’s personal blog -> https://blog.jonathanchannon.com/2020-07-17-understanding-fsharp-applicatives-custom-operators]
After discussing something with Ian Russell he suggested I take some time to read through another fine blog post he has written and understand F# applicatives and custom operators. I found myself in familiar territory when reading F# blog posts and it's something similar to the five stages of grief. Nod, Nod, I understand what's going on, Umm, WTF is going on. As Ian did in his Intro to F# series he sets out a simple domain problem and goes about how to address it. We want to return a ValidatedUser
from a function but if the user fails validation we return a list of validation errors.
The code in the blog post was pretty self explanatory until, it wasn't, which I have pasted below:
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:string) = 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
As you can see, there is commented out code on the last line because he has lined up the 3 arguments that are required to call the create
function but calling it as-is won't work because the function takes in string,string,DateTime
and we have Result<string, ValidationFailure list>, Result<string, ValidationFailure list>,Result<DateTime, ValidationFailure list>
. As we know from my previous blog post we can use the Result.map
function to do this sort of thing.
I will skip to the solution to this and work backwards because this is where I started to scratch my head a lot! Luckily the F# Software Foundation slack channel helped a lot in particular Paul Blasucci
create |> Result.map <| validatedName |> apply <| validatedEmail |> apply <| validatedDateOfBirth
From the last blog post I showed how to call functions in a chain of functions where Result
types needed to be unwrapped and their values passed to the next function. So my first thought looking at this was validatedName
is a value not a function so how is Result.map
working? I also didn't quite understand the precedence of |>
and |<
data-preserve-html-node="true" how that worked. As part of my investigation, or some may say my learning and understanding, I was told Don Syme regretted making the back pipe and that using forward and back pipes together can make code unreadable. The take away there is to be careful about it's usage. The good thing here is that we only have one usage of it but it still didn't make sense to me. So I tried to split it up:
let foo = create |> Result.map <| validatedName
I still didn't quite get it, foo
is a type of Result<(string -> DateTime -> ValidatedUser), ValidationFailure list>
which means it's taken the name argument and now wants the email and date of birth passed to it. I understood partial application but still it didn't click. I went back to the previous blog post and looked at what the function signature of Result.map
is. It takes in a function and a Result<'a,'b>
. If the Result
is OK
it calls the passed in function with the unwrapped Result
of 'a
and returns a Result
type of Ok(fn a)
otherwise it returns Error e
Here's the code for it:
let map mapping result = match result with Error e -> Error e | Ok x -> Ok (mapping x)
I then went back to the line of code after being informed that |<
data-preserve-html-node="true" will always get called after |>
. So what we have is create
is passed in as the function to call in Result.map
and the Result
type is the validatedName
variable. PARTIAL APPLICATION!!! Ok I get it now!
So once I could see what was happening it was time to understand what the apply
function was doing. The first argument is a Result
type whose generic args were a function and a list of validation failures, the second argument was a Result
type whose generic args were a value and a list of validation failures. What apply
does is match the two Result types together to check for (Ok, Ok)
or (Ok, Error)
etc and on success call the unwrapped function of the first arg with the unwrapped value of the second arg.
What confused me here was F# compiler magic. Now I knew about partial application but what I didn't understand was that when you assign a variable by calling a function using partial application the resulting type is not the result of the function being called. It's just a type of the function with one less argument to call, the compiler knows when to call the actual function once all arguments have been passed to it. What the function is doing is chaining argument calls to a partial application function. So we can see:
let foo = create |> Result.map <| validatedName // Result.map create validatedName let bar = foo |> apply <| validatedEmail // apply foo validatedEmail let baz = bar |> apply <| validatedDateOfBirth // apply bar validatedDateOfBirth
baz
now is the final result of a call to execute the create
function.
We can then remove the let
s above and get to the final solution I mentioned previously:
create |> Result.map <| validatedName |> apply <| validatedEmail |> apply <| validatedDateOfBirth
As mentioned above it's advised not to use back pipes and so what we could end up with is :
create <!> validatedName <*> validatedEmail <*> validatedDateOfBirth
If like me you were totally confused by this then please see Ian's blog post for a full explanation but here's my take away.
As we saw above the apply function takes an "elevated" function and "elevated" value and then calls the function with the value and returns an elevated result. So we know in long hand version we have:
apply (apply (Result.map create validatedName) validatedEmail) validatedDateOfBirth
We can also use operators to replace function names to tidy things up so we end up with:
let (<!>) = Result.map let (<*>) = apply
And we can make the above look like:
(<*>) ((<*>) ((<!>) create validatedName) validatedEmail) validatedDateOfBirth
This hopefully all makes sense, but now a slight lesson in math notation which blew my mind. We know the signature 1 + 1 = 2
. However, +
is actually a function that takes two numbers, so in that regard what you have known since you were aged three should look like + 1 1
if we were to apply common programming signatures. Interestingly, you could also call the +
function like 1 1 +
. Where the +
sits in the signature is called notation. Typically programming languages will use "prefix notation" function arg1 arg2
and some may use "postfix notation" arg1 arg2 function
and arithmetic generally uses "infix notation" arg1 function arg2
. However, in F# you can use "infix notation" which looks like the typical 1 + 1
signature which is let add x y = x + y
. If we replace x + y
knowing that the +
is the function we can go from "prefix notation" (<!>) create validatedName
to "infix notation" create <!> validatedName
and apply it to our functions above. As we apply the calls to our infixed functions what we end up with is:
create <!> validatedName <*> validatedEmail <*> validatedDateOfBirth
This looks much neater than apply (apply (Result.map create validatedName) validatedEmail) validatedDateOfBirth
but it does take a bit of learning and re-thinking to work out how the final solution create <!> validatedName <*> validatedEmail <*> validatedDateOfBirth
actually works. I know this has been quite a learning curve for me but thankfully there are resources, people in the F# community and colleagues (thanks Ian!) that are keen to help and I thank them very much for this!