Skip to content

Instantly share code, notes, and snippets.

@mrange
Last active October 14, 2018 07:39
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mrange/eff7a35a2d601a59c9b73fb8aaa18b16 to your computer and use it in GitHub Desktop.
Save mrange/eff7a35a2d601a59c9b73fb8aaa18b16 to your computer and use it in GitHub Desktop.
F# Computation Expressions are syntactic sugar for Monads

Related to Monads are F# computation expressions (CE). A programmer typically implements a CE to provide an alternative approach to chaining Monads, ie instead of this:

    let v = m >>= fun x -> n >>= fun y -> return_ (x, y)

(As usual >>= is monadic bind)

You can write this:

    let v = ce {
        let! x = m
        let! y = n
        return x, y
      }

Both styles are equivalent and it's up to developer preference which one to pick.

In order to demonstrate how to implement a CE imagine you like to develop a trace library where all traces should include a correlation id. This correlation id will help correlating traces that belong to the same call. This is very useful when have log files that contains traces from concurrent calls.

The problem is that it's cumbersome to include the correlation id as an argument to all functions. As Monads allows carrying implicit state we will define a Log Monad to hide the log context (ie the correlation id).

We begin by defining a log context and the type of a function that traces with log context:

    type Context =
      {
        CorrelationId : Guid
      }
      static member New () : Context = { CorrelationId = Guid.NewGuid () }

    type Function<'T> = Context -> 'T

    // Runs a Function<'T> with a new log context
    let run t = t (Context.New ())

We also define two trace functions that will log with the correlation id from the log context:

    let trace v   : Function<_> = fun ctx -> printfn "CorrelationId: %A - %A" ctx.CorrelationId v
    let tracef fmt              = kprintf trace fmt

trace is a Function<unit> which means it will be passed a log context when invoked. From the log context we pick up the correlation id and traces it together with v

In addition, we define bind and return_ and as they follow the Monad Laws this forms our Log Monad.

    let bind t uf : Function<_> = fun ctx ->
      let tv = t ctx  // Invoke t with the log context
      let u  = uf tv  // Create u function using result of t
      u ctx           // Invoke u with the log context
    
    // >>= is the common infix operator for bind
    let inline (>>=) (t, uf) = bind t uf
    
    let return_ v : Function<_> = fun ctx -> v

Finally we define LogBuilder that will enable us to use CE syntax to chain Log Monads.

    type LogBuilder() =
      member x.Bind   (t, uf) = bind t uf
      member x.Return v       = return_ v

    // This enables us to write function like: let f = log { ... }
    let log = Log.LogBuilder ()

We can now define our functions that should have the implicit log context:

    let f x y =
      log {
        do! Log.tracef "f: called with: x = %d, y = %d" x y
        return x + y
      }

    let g =
      log {
        do! Log.trace "g: starting..."
        let! v = f 1 2
        do! Log.tracef "g: f produced %d" v
        return v
      }

We execute g with:

    printfn "g produced %A" (Log.run g)

Which prints:

CorrelationId: 33342765-2f96-42da-8b57-6fa9cdaf060f - "g: starting..."
CorrelationId: 33342765-2f96-42da-8b57-6fa9cdaf060f - "f: called with: x = 1, y = 2"
CorrelationId: 33342765-2f96-42da-8b57-6fa9cdaf060f - "g: f produced 3"
g produced 3

Notice that the CorrelationId is implicitly carried from run to g to f which allows us the correlate the log entries during trouble shooting.

CE has lot more features but this should help you get started defining your own CE:s.

Full code:

    module Log =
      open System
      open FSharp.Core.Printf

      type Context =
        {
          CorrelationId : Guid
        }
        static member New () : Context = { CorrelationId = Guid.NewGuid () }

      type Function<'T> = Context -> 'T

      // Runs a Function<'T> with a new log context
      let run t = t (Context.New ())

      let trace v   : Function<_> = fun ctx -> printfn "CorrelationId: %A - %A" ctx.CorrelationId v
      let tracef fmt              = kprintf trace fmt

      let bind t uf : Function<_> = fun ctx ->
        let tv = t ctx  // Invoke t with the log context
        let u  = uf tv  // Create u function using result of t
        u ctx           // Invoke u with the log context

      // >>= is the common infix operator for bind
      let inline (>>=) (t, uf) = bind t uf

      let return_ v : Function<_> = fun ctx -> v

      type LogBuilder() =
        member x.Bind   (t, uf) = bind t uf
        member x.Return v       = return_ v

    // This enables us to write function like: let f = log { ... }
    let log = Log.LogBuilder ()

    let f x y =
      log {
        do! Log.tracef "f: called with: x = %d, y = %d" x y
        return x + y
      }

    let g =
      log {
        do! Log.trace "g: starting..."
        let! v = f 1 2
        do! Log.tracef "g: f produced %d" v
        return v
      }

    [<EntryPoint>]
    let main argv =
      printfn "g produced %A" (Log.run g)
      0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment