F# 计算表达式提供了链接Monad的另一种语法

示例

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

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用作所有函数的参数很麻烦。由于Monad允许携带隐式状态,我们将定义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.CorrelationIdv
let tracef fmt              = kprintf trace fmt

trace是aFunction<unit>表示在调用时将传递日志上下文。从日志上下文中,我们选择相关性ID并将其与v

此外,我们定义bind并return_遵循Monad法则,这构成了Log Monad。

let bind t uf : Function<_> = fun ctx ->
  let tv = t ctx  // 使用日志上下文调用t
  let u  = uf tv  // 使用t的结果创建u函数
  u ctx           // 使用日志上下文调用u

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

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

最后,我们定义LogBuilder了将使我们能够使用CE语法来链接LogMonad的方法。

type LogBuilder() =
  memberx.Bind  (t, uf) = bind t uf
  memberx.Returnv       = return_ v

// 这使我们能够编写如下函数: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到的g,f这使我们可以在故障排除期间将日志条目相关联。

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.CorrelationIdv
  let tracef fmt              = kprintf trace fmt

  let bind t uf : Function<_> = fun ctx ->
    let tv = t ctx  // 使用日志上下文调用t
    let u  = uf tv  // 使用t的结果创建u函数
    u ctx           // 使用日志上下文调用u

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

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

  type LogBuilder() =
    memberx.Bind  (t, uf) = bind t uf
    memberx.Returnv       = return_ v

// 这使我们能够编写如下函数: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