Computation Expressions 为链 Monad 提供了另一种语法

与 Monads 相关的是 F# 计算表达式CE)。程序员通常实现 CE 来提供链接 Monads 的替代方法,即代替:

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

你可以这样写:

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

这两种风格都是等价的,这取决于开发者的偏好。

为了演示如何实现 CE,想象你希望所有跟踪都包含相关 ID。此关联 ID 将帮助关联属于同一调用的跟踪。当包含来自并发调用的跟踪的日志文件时,这非常有用。

问题是将相关 id 作为参数包含在所有函数中是很麻烦的。由于 Monads 允许携带隐式状态, 我们将定义一个 Log Monad 来隐藏日志上下文(即相关 id)。

我们首先定义日志上下文和跟踪日志上下文的函数类型:

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 ())

我们还定义了两个跟踪函数,这些函数将使用日志上下文中的相关 ID 进行记录:

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

trace 是一个 Function<unit>,这意味着它将在调用时传递日志上下文。从日志上下文中我们获取相关 ID 并将其与 v 一起跟踪

此外,我们定义了 bindreturn_,当他们遵循 Monad Laws 时, 这形成了我们的 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

最后我们定义了 LogBuilder,这将使我们能够使用 CE 语法链接 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 ()

我们现在可以定义应该具有隐式日志上下文的函数:

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
  }

我们执行 g:

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

哪个打印品:

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

请注意,CorrelationId 隐含地从 run 传送到 gf,这允许我们在故障排除期间关联日志条目。

CE很多功能, 但这可以帮助你开始定义自己的 CE:s。

完整代码:

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