Computation Expressions in F#

Posted March 11, 2021 by Rafał Gwoździński ‐ 6 min read

Computation Expressions in F#

Computation expression is a syntax to express computations with monads (as well as with monoids, monad transformers and applicative functors). Let's see how we can use it to work with monadic values. We will try to figure out both easy and more complicated cases (e.g. nested values). We will use FSharpPlus library that provide set of general monad expressions, and compare them to "plain" F# approach, to see how it differs.

Additive monad expression

Let's start with a simple example of concatenation. We have two lists and a function that takes a list and returns a new list.

let l1 = [1;2;3]
let l2 = [4;5;6]
let listFun  = bind (fun x -> [x;x;x])

A simple way to concatenate them is to use @ operatior

let lConcat = l1 @ listFun l2

When dealing with more complicated examples, like concatentation of both lists and plain values, we can use list computation expression from F# core library.

let lConcat' = 
    [ yield! l1
      yield! listFun l2
      yield 42 ] 

Instead of dedicated expression, we can just use generic additive monad expression. It works for seq and array types too.

let lConcat'' = 
    monad.plus {
        return! l1
        return! listFun l2
        return 42 }

Effectful monad expression

Second type of generic monad expression is used to handle monads that have embedded side-effects.

Async

First, let's look at Async monad handling. Plain approach uses dedicated async expression:

let async1 = async { return 1 }
let async2 = async { return 2 }

let aResult = 
    async {
        let! x1 = async1
        let! x2 = async2
        return x1 + x2
    } 

We can substitute it with generic F#+ monad expression:

let aResult' = 
    monad {
        let! x1 = async1
        let! x2 = async2
        return x1 + x2 
    } 

Performing this operations without computation expressions is cumbersome. We substitute let! with async.Bind and return with async.Return, which gives us:

let aResult'' = async.Bind(async1, 
                    fun x1 -> async.Bind(async2, 
                                 fun x2 -> async.Return (x1 + x2)

We can make it a bit cleaner with F#+ operators:

let aResult''' = async1 >>= fun x1 -> 
                    async2 >>= fun x2 -> 
                        x1 + x2 |> async.Return

But for simple operations we can just use F#+ map2

let aResult'''' = Async.map2 (+) async1 async2

or lift2

let aResult''''' = lift2 (+) async1 async2

Option

There is no Option computation expression in F# core, so we have to use F#+:

let opt1 = Some 1
let opt2 = Some 2

let oResult = monad {
    let! x1 = opt1
    let! x2 = opt2
    return x1 + x2 }

We can use bind/map/lift as with async:

let oResult' = 
    opt1 |> Option.bind (fun x1 -> 
        opt2 |> Option.bind (fun x2 -> x1 + x2 |> Some)) 

let oResult''  = lift2       (+) opt1 opt2
let oResult''' = Option.map2 (+) opt1 opt2

We can also pattern match:

let oResult'''' =
    match (opt1, opt2) with
    | (Some opt1, Some opt2) -> Some (opt1 + opt2)
    | (_,_)                  -> None

Computation expressions are especially useful for complicated operations:

let opt3 = Some 3
let add1 = (+) 1
let isEven x = if x % 2 = 0                                 
               then Some x 
               else None
let multiplyBy4 = (*) 4

let harderOptResult = 
    monad {
        let! x1 = opt3
        let! even = x1 |> add1 |> isEven
        return multiplyBy4 even }

In this case bind/map approach doesn't look that bad:

let harderOptResult' = 
    opt3 
    |> Option.bind (add1 >> isEven) 
    |> Option.map multiplyBy4

Pattern matching on Options doesn't scale well. It gets unreadable even with one level of nesting.

let harderOptResult'' = 
    match opt3 with
    | None -> None
    | Some v -> v |> add1 |> isEven
                |> function 
                    | None ->   None
                    | Some e -> Some <| multiplyBy4 e

Result

Works similarly to Option:

let res1 : Result<int,string> = Ok 1
let res2 : Result<int,string> = Error "Niepowodzenie"

let rResult = monad {
    let! x1 = res1
    let! x2 = res2
    return x1 + x2 }

let rResult'  = Result.map2 (+) res1 res2
let rResult'' = lift2       (+) res1 res2 

Pattern matching on Results is laborous, because we have to handle both errors manually:

let rResult''' =
    match (res1, res2) with
    | (Ok res1, Ok res2) -> Ok <| res1 + res2
    | (Error e ,_)       -> Error e
    | (_, Error e)       -> Error e

Effectful Result

Let's see what happens when we have effectful function that returns Result of unit. Instead of let!, we use do! keyword.

let eRes1 () = 
    printfn "computing res1"
    Ok ()

let eRes2 () = Error "Could not compute res2"

let effResult = 
    monad {
        do! eRes1 ()
        do! eRes2 () }

Without computation expression, we get code thats pretty awkward

let effResult' = eRes1 () 
                 |> Result.bind (fun () -> eRes2 () 
                                           |> Result.bind (fun () -> Ok ()))

F#+ bind operator doesn't help

let effResult'' = eRes1 () 
                  >>= fun () -> eRes2 () 
                                >>= (fun () -> Ok ())

Code

#r "nuget: FSharpPlus"

open FSharpPlus

// Additive monad expression

let l1 = [1;2;3]
let l2 = [4;5;6]
let listFun  = bind (fun x -> [x;x;x])

let lConcat = l1 @ listFun l2

let lConcat' = 
    [ yield! l1
      yield! listFun l2
      yield 42 ] 

let lConcat'' = 
    monad.plus {
        return! l1
        return! listFun l2
        return 42 }

// Effectful monad expression

//// Async
let async1 = async { return 1 }
let async2 = async { return 2 }

let aResult = 
    async {
        let! x1 = async1
        let! x2 = async2
        return x1 + x2
    } 
aResult |> Async.RunSynchronously

let aResult' = 
    monad {
        let! x1 = async1
        let! x2 = async2
        return x1 + x2 
    } 
aResult' |> Async.RunSynchronously

let aResult'' = async.Bind(async1, 
                    fun x1 -> async.Bind(async2, 
                                 fun x2 -> async.Return (x1 + x2)))
aResult'' |> Async.RunSynchronously

let aResult''' = async1 >>= fun x1 -> 
                    async2 >>= fun x2 -> 
                        x1 + x2 |> async.Return
aResult''' |> Async.RunSynchronously

let aResult'''' = Async.map2 (+) async1 async2
                  |> Async.RunSynchronously

let aResult''''' = lift2 (+) async1 async2
                   |> Async.RunSynchronously


//// Option
let opt1 = Some 1
let opt2 = Some 2

let oResult = monad {
    let! x1 = opt1
    let! x2 = opt2
    return x1 + x2 }

let oResult' = 
    opt1 |> Option.bind (fun x1 -> 
        opt2 |> Option.bind (fun x2 -> x1 + x2 |> Some)) 

let oResult''  = lift2       (+) opt1 opt2
let oResult''' = Option.map2 (+) opt1 opt2

let oResult'''' =
    match (opt1, opt2) with
    | (Some opt1, Some opt2) -> Some <| opt1 + opt2
    | (_,_)                  -> None


//// Option - complicated
let opt3 = Some 3
let add1 = (+) 1
let isEven x = if x % 2 = 0                                 
               then Some x 
               else None
let multiplyBy4 = (*) 4

let harderOptResult = 
    monad {
        let! x1 = opt3
        let! even = x1 |> add1 |> isEven
        return multiplyBy4 even }

let harderOptResult' = 
    opt3 
    |> Option.bind (add1 >> isEven) 
    |> Option.map multiplyBy4

let harderOptResult'' = 
    match opt3 with
    | None -> None
    | Some v -> v |> add1 |> isEven
                |> function 
                    | None ->   None
                    | Some e -> Some <| multiplyBy4 e

//// Result
let res1 : Result<int,string> = Ok 1
let res2 : Result<int,string> = Error "Niepowodzenie"

let rResult = monad {
    let! x1 = res1
    let! x2 = res2
    return x1 + x2 }

let rResult'  = Result.map2 (+) res1 res2
let rResult'' = lift2       (+) res1 res2 

let rResult''' =
    match (res1, res2) with
    | (Ok res1, Ok res2) -> Ok <| res1 + res2
    | (Error e ,_)       -> Error e
    | (_, Error e)       -> Error e


//// Effectful Result
let eRes1 () = 
    printfn "computing res1"
    Ok ()

let eRes2 () = Error "Could not compute res2"

let effResult = 
    monad {
        do! eRes1 ()
        do! eRes2 () }

let effResult' = eRes1 () 
                 |> Result.bind (fun () -> eRes2 () 
                                           |> Result.bind (fun () -> Ok ()))

let effResult'' = eRes1 () 
                  >>= fun () -> eRes2 () 
                                >>= (fun () -> Ok ())