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