DEV Community

Cover image for F# applicative computation expressions
Romain Deneau
Romain Deneau

Posted on

F# applicative computation expressions

This fifth article in the series dedicated to F# computation expressions is a guide to writing F# computation expressions having an applicative behavior.

Table of contents

Introduction

An applicative CE is revealed through the usage of the and! keyword (F# 5).

Builder method signatures

An applicative CE builder should define these methods:

// Method        | Signature                        | Equivalence
    MergeSources : mx: M<X> * my: M<Y> -> M<X * Y>  ; map2 (fun x y -> x, y) mx my
    BindReturn   : m: M<T> * f: (T -> U) -> M<U>    ; map f m
Enter fullscreen mode Exit fullscreen mode

CE Applicative example - validation {}

type Validation<'t, 'e> = Result<'t, 'e list>

type ValidationBuilder() =
    member _.BindReturn(x: Validation<'t, 'e>, f: 't -> 'u) =
        Result.map f x

    member _.MergeSources(x: Validation<'t, 'e>, y: Validation<'u, 'e>) =
        match (x, y) with
        | Ok v1,    Ok v2    -> Ok(v1, v2)     // Merge both values in a pair
        | Error e1, Error e2 -> Error(e1 @ e2) // Merge errors in a single list
        | Error e, _ | _, Error e -> Error e   // Short-circuit single error source

let validation = ValidationBuilder()
Enter fullscreen mode Exit fullscreen mode

Usage: validate a customer

  • Name not null or empty
  • Height strictly positive
type [<Measure>] cm
type Customer = { Name: string; Height: int<cm> }

let validateHeight height =
    if height <= 0<cm>
    then Error ["Height must be positive"]
    else Ok height

let validateName name =
    if System.String.IsNullOrWhiteSpace name
    then Error ["Name can't be empty"]
    else Ok name

module Customer =
    let tryCreate name height : Result<Customer, string list> =
        validation {
            let! validName = validateName name
            and! validHeight = validateHeight height
            return { Name = validName; Height = validHeight }
        }

let c1 = Customer.tryCreate "Bob" 180<cm>  // Ok { Name = "Bob"; Height = 180 }
let c2 = Customer.tryCreate "Bob" 0<cm> // Error ["Height must be positive"]
let c3 = Customer.tryCreate "" 0<cm>    // Error ["Name can't be empty"; "Height must be positive"]
Enter fullscreen mode Exit fullscreen mode

Desugaring:

validation {                                ; validation.BindReturn(
                                            ;     validation.MergeSources(
    let! name = validateName "Bob"          ;         validateName "Bob",
    and! height = validateHeight 0<cm>      ;         validateHeight 0<cm>
                                            ;     ),
    return { Name = name; Height = height } ;     (fun (name, height) -> { Name = name; Height = height })
}                                           ; )
Enter fullscreen mode Exit fullscreen mode

Trap

⚠️ The compiler accepts that we define ValidationBuilder without BindReturn but with Bind and Return. But in this case, we can lose the applicative behavior and it enables monadic CE bodies!

FsToolkit validation {}

FsToolkit.ErrorHandling offers a similar validation {}.

The desugaring reveals the definition of more methods: Delay, Run, Source📍

validation {                                ;  validation.Run(
    let! name = validateName "Bob"          ;      validation.Delay(fun () ->
    and! height = validateHeight 0<cm>      ;          validation.BindReturn(
    return { Name = name; Height = height } ;              validation.MergeSources(
}                                           ;                  validation.Source(validateName "Bob"),
                                            ;                  validation.Source(validateHeight 0<cm>)
                                            ;              ),
                                            ;              (fun (name, height) -> { Name = name; Height = height })
                                            ;          )
                                            ;      )
                                            ;  )
Enter fullscreen mode Exit fullscreen mode

Source methods

In FsToolkit validation {}, there are a couple of Source methods defined:

  • The main definition is the id function.
  • Another overload is interesting: it converts a Result<'a, 'e> into a Validation<'a, 'e>. As it's defined as an extension method, it has a lower priority for the compiler, leading to a better type inference. Otherwise, we would need to add type annotations.

☝️ Note: Source documentation is scarce. The most valuable information comes from a question on Stack Overflow mentioned in FsToolkit source code!

Conclusion

Applicative computation expressions in F# enable parallel computation and error accumulation through the and! syntax introduced in F# 5. By implementing MergeSources and BindReturn methods, you can create powerful validation workflows that collect all errors rather than stopping at the first failure, as well as performant computations that leverage parallelism. This approach is particularly valuable for form validation, configuration parsing, and any scenario where you want to provide comprehensive feedback to users about multiple validation failures simultaneously. While applicative CEs are less versatile than monadic ones, they excel in specific use cases where their distinct capabilities make a significant difference.

Top comments (0)