Understanding F# applicatives and custom operators

Jonathan Channon discusses how he learnt about a slightly more advanced functional concept in F# — Applicatives.

[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
    match input with
    | IsValidName -> Ok input
    | _ -> Error [ NameIsInvalidFailure ]

let validateEmail input = // string -> Result
    match input with
    | IsValidEmail email -> Ok email
    | _ -> Error [ EmailIsInvalidFailure ]

let validateDateOfBirth input = // string -> Result
    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 =
    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 lets 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!

Blog 8/7/20

Understanding F# Type Aliases

In this post, we discuss the difference between F# types and aliases that from a glance may appear to be the same thing.

Blog 10/11/22

Introduction to Functional Programming in F# – Part 5

Master F# asynchronous workflows and parallelism. Enhance application performance with advanced functional programming techniques.

Blog 5/18/22

Introduction to Functional Programming in F#

Dive into functional programming with F# in our introductory series. Learn how to solve real business problems using F#'s functional programming features. This first part covers setting up your environment, basic F# syntax, and implementing a simple use case. Perfect for developers looking to enhance their skills in functional programming.

Blog 9/27/22

Creating solutions and projects in VS code

In this post we are going to create a new Solution containing an F# console project and a test project using the dotnet CLI in Visual Studio Code.

Blog 11/30/22

Introduction to Partial Function Application in F#

Partial Function Application is one of the core functional programming concepts that everyone should understand as it is widely used in most F# codebases.In this post I will introduce you to the grace and power of partial application. We will start with tupled arguments that most devs will recognise and then move onto curried arguments that allow us to use partial application.

Blog 5/1/21

Ways of Creating Single Case Discriminated Unions in F#

There are quite a few ways of creating single case discriminated unions in F# and this makes them popular for wrapping primitives. In this post, I will go through a number of the approaches that I have seen.

Training

Getting more from Jira Workflows (Data Center)

Over the course of "Getting More from Jira Workflows (Data Center)" training participants learn about common status and transition properties, advanced workflow functionalities and how to configure them.

Blog

International cooperation: Insights from Michael

In our latest interview, we present the multifaceted everyday working life of Mike Diamant, Senior Atlassian Consultant at catworkx. With over 30 years of IT experience and a penchant for a good joke, he talks about what working at catworkx feels like for him as an American, what he appreciates about the German work culture and why working with customers and colleagues is so special for him.

Training

Getting More from Jira Workflows (Cloud)

Over the course of the "Getting More from Jira Workflows (Cloud)" training participants will learn about common status and transition properties and advanced workflow functionalities and how to configure them.

Training

Getting More from Jira Workflows (Cloud)

Over the course of the "Getting More from Jira Workflows (Cloud)" training participants will learn about common status and transition properties and advanced workflow functionalities and how to configure them.

Referenz

Customer Relationship Management with Jira and Confluence

TOPMOTIVE Group, a leading provider of catalog and information systems in the automotive aftermarket, used Atlassian tools to bundle and provide sales-related information in one system.

Die Videokonferenzlösung von Google Workspace Meet
Unternehmen

Customer Portal

CLOUDPILOTS Customer Portal.

Referenz

Custom licensing

MARKANT Handels und Service GmbH (MARKANT) is fully exploiting the potential of its IBM software licenses with this year's license renewal. Instead of relying on IBM's traditional Passport Advantage model as in the past, MARKANT is using a licensing concept specially adapted to the company for the first time.

Service

Customer Experience​ & Retention​

Promote long-term customer relationships through targeted retention strategies based on excellent customer experience.

Commerce & Customer Experience, CRM
Service

Commerce & Customer Experience, CRM

In trade, a positive customer experience (CX) is of central importance for the continued success of a company. To achieve this, the customer's expectations must be met at all touch points.

Kompetenz 2/10/20

Certificates and awards

Our company and our products have won awards in the truest sense of the word. Among other things, SAP has certified our software solutions several times with various seals of approval.

Service

Digitalization and cloud transformation

We adjust the levers of your individual digitalization initiative for speed, adaptability, security and compliance!

Kompetenz 7/29/21

DevOps and CI/CD

A DevOps introduction gains momentum and focus with the support of our experts. We record the initiatives and capture the context of the company.

HCL Leap & Volt
Technologie

HCL Leap and Volt

An intranet has the potential for a fully functional digital workplace. To achieve this, the individual user must have access to various tools.

Headerbild zu Tempo Customizing und Integration
Technologie

Tempo Customizing and Integration

Adapt Tempo for Jira to your needs and integrate your Atlassian time recording into the ERP landscape, such as SAP or HCL Domino, seamlessly.

Bleiben Sie mit dem TIMETOACT GROUP Newsletter auf dem Laufenden!