F# monadic computation expressions
Romain Deneau

Romain Deneau @rdeneau

About: Big fan of F#, Clean Code, DDD. Senior Software Crafters, Full-stack Developer and Trainer @ D-EDGE (Paris, France)

Location:
Paris, France
Joined:
Feb 4, 2019

F# monadic computation expressions

Publish Date: Aug 22
0 0

This fourth article in the series dedicated to F# computation expressions is a guide to writing F# computation expressions having a monadic behavior.

Table of contents

Introduction

A monadic CE can be identified by the usage of let! and return keywords, revealing the monadic bind and return operations.

Builder method signatures

Behind the scenes, builders of these CEs should/can implement these methods:

// Method     | Signature                                     | CE syntax supported
    Bind      : M<T> * (T -> M<U>) -> M<U>                    ; let! x = xs in ...
                (* when T = unit *)                           ; do! command
    Return    : T -> M<T>                                     ; return x
    ReturnFrom: M<T> -> M<T>                                  ; return!

// Additional methods
    Zero      : unit -> M<T>                                  ; if // without `else` // Typically `unit -> M<unit>`
    Combine   : M<unit> * M<T> -> M<T>                        ; e1; e2  // e.g. one loop followed by another one
    TryWith   : M<T> -> (exn -> M<T>) -> M<T>                 ; try/with
    TryFinally: M<T> * (unit -> M<unit>) -> M<T>              ; try/finally
    While     : (unit -> bool) * (unit -> M<unit>) -> M<unit> ; while cond do command ()
    For       : seq<T> * (T -> M<unit>) -> M<unit>            ; for i in xs do command i ; for i = 0 to n do command i
    Using     : T * (T -> M<U>) -> M<U> when T :> IDisposable ; use! x = xs in ...
Enter fullscreen mode Exit fullscreen mode

Monadic vs Monoidal

Return (monadic) vs Yield (monoidal)

  • Same signature: T -> M<T>
  • A series of return is not expected → Monadic Combine takes only a monadic command M<unit> as 1st param
  • CE enforces appropriate syntax by implementing one of these methods:
    • seq {} allows yield but not return
    • async {}: the reverse

For and While

Method CE Signature
For Monoidal seq<T> * (T -> M<U>) -> M<U> or seq<M<U>>
Monadic seq<T> * (T -> M<unit>) -> M<unit>
While Monoidal (unit -> bool) * Delayed<T> -> M<T>
Monadic (unit -> bool) * (unit -> M<unit>) -> M<unit>

👉 Different use cases:

  • Monoidal: Comprehension syntax
  • Monadic: Series of effectful commands

CE monadic and delayed

Like monoidal CE, monadic CE can use a Delayed<'t> type.
→ Impacts on the method signatures:

 Delay      : thunk: (unit -> M<T>) -> Delayed<T>
 Run        : Delayed<T> -> M<T>
 Combine    : M<unit> * Delayed<T> -> M<T>
 While      : predicate: (unit -> bool) * Delayed<unit> -> M<unit>
 TryFinally : Delayed<T> * finalizer: (unit -> unit) -> M<T>
 TryWith    : Delayed<T> * handler: (exn -> unit) -> M<T>
Enter fullscreen mode Exit fullscreen mode

CE monadic examples

☝️ The initial CEs studied—logger {} and option {}—were monadic.

CE monadic example - result {}

Let's build a result {} CE to play with dice!

type ResultBuilder() =
    member _.Bind(rx, f) = rx |> Result.bind f
    member _.Return(x) = Ok x
    member _.ReturnFrom(rx) = rx

let result = ResultBuilder()

// ---

let rollDice =
    let random = Random(Guid.NewGuid().GetHashCode())
    fun () -> random.Next(1, 7)

let tryGetDice dice =
    result {
        if rollDice() <> dice then
            return! Error $"Not the expected dice {dice}."
    }

let tryGetAPairOf6 =
    result {
        let n = 6
        do! tryGetDice n
        do! tryGetDice n
        return true
    }
Enter fullscreen mode Exit fullscreen mode

Desugaring:

let tryGetAPairOf6 =
    result {                ;
        let n = 6           ;   let n = 6
        do! tryGetDice n    ;   result.Bind(tryGetDice n, (fun () ->
        do! tryGetDice n    ;        result.Bind(tryGetDice n, (fun () ->
        return true         ;            result.Return(true)
    }                       ;        ))
                            ;   ))
Enter fullscreen mode Exit fullscreen mode

CE monadic: FSharpPlus monad {}

FSharpPlus provides a monad CE

  • Works for all monadic types: Option, Result, ... and even Lazy 🎉
  • Supports monad stacks with monad transformers 📍

⚠️ Limits:

  • Confusing: the monad CE has 4 flavours to cover all cases: delayed or strict, embedded side-effects or not
  • Based on SRTP: can be very long to compile!
  • Documentation not exhaustive, relying on Haskell knowledges
  • Very Haskell-oriented: not idiomatic F#

Monad stack, monad transformers

A monad stack is a composition of different monads.
→ Example: Async+Option.

We can handle it with 2 styles: academic or F# idiomatic.

1. Academic style (with FSharpPlus)

Monad transformer (here MaybeT)
→ Extends Async to handle both effects
→ Resulting type: MaybeT<Async<'t>>

✅ Reusable with other inner monads
❌ Less easy to evaluate the resulting value
❌ Not idiomatic

2. Idiomatic style

Custom CE asyncOption, based on the async CE, handling the Async<Option<'t>> type

type AsyncOption<'T> = Async<Option<'T>> // Convenient alias, not required

type AsyncOptionBuilder() =
    member _.Bind(aoX: AsyncOption<'a>, f: 'a -> AsyncOption<'b>) : AsyncOption<'b> =
        async {
            match! aoX with
            | Some x -> return! f x
            | None -> return None
        }

    member _.Return(x: 'a) : AsyncOption<'a> =
        async { return Some x }
Enter fullscreen mode Exit fullscreen mode

⚠️ Limits: Not reusable, just copiable for asyncResult, for instance

Conclusion

Monadic computation expressions in F# provide a familiar syntax—based on let! and return keywords—for sequencing effectful computations. While you can build custom monadic CEs for specific domain needs or leverage libraries like FSharpPlus for academic-style programming, the most practical approach is often creating idiomatic F# builders tailored to your specific monad combinations, such as Async<Option<'t>> or Async<Result<'t, 'e>>. This strikes the right balance between expressiveness and maintainability in typical F# codebases.

Comments 0 total

    Add comment