【问题标题】:How to implement delay in the maybe computation builder?如何在可能的计算构建器中实现延迟?
【发布时间】:2012-01-29 00:46:19
【问题描述】:

这是我目前所拥有的:

type Maybe<'a> = option<'a>

let succeed x = Some(x)

let fail = None

let bind rest p =
    match p with
        | None -> fail
        | Some r -> rest r

let rec whileLoop cond body =
    if cond() then
        match body() with
        | Some() ->
            whileLoop cond body
        | None ->
            fail
    else
        succeed()

let forLoop (xs : 'T seq) f =
    using (xs.GetEnumerator()) (fun it ->
            whileLoop
                (fun () -> it.MoveNext())
                (fun () -> it.Current |> f)
        )

whileLoop 可以很好地支持 for 循环,但我不知道如何获得支持的 while 循环。部分问题是while循环的翻译使用delay,在这种情况下我无法弄清楚。下面明显的实现可能是错误的,因为它不会延迟计算,而是运行它!

let delay f = f()

没有延迟也会阻碍try...withtry...finally

【问题讨论】:

  • 你试过let delay f = fun () -&gt; f()吗?
  • 你看过Maybe在FSharpx github.com/fsharp/fsharpx/blob/master/src/FSharpx.Core/Monad.fs中的monad实现了吗?
  • 查看stackoverflow.com/questions/4577050/… 了解一种方法。
  • 特别是,Delay f = fRun f = f() 应该可以工作,假设您相应地调整了 WhileCombine 等的类型。
  • @Joh 你可以用(unit -&gt; M&lt;'T&gt;) -&gt; (unit -&gt; M&lt;'T&gt;) 类型定义delay(正如@kvb 所建议的那样)。如果您修改其余定义以匹配此类型,一切都会正常工作。有关更复杂的定义,请参阅我的答案。 (诀窍是计算构建器可以简单地使用不同的类型进行非延迟和延迟计算)。

标签: f# computation-expression


【解决方案1】:

在 F# 中实现延续构建器实际上有两种不同的方式。一种是使用 monadic 类型表示延迟计算(如果它支持某种表示延迟计算的方式,如 Async&lt;'T&gt; 或 kkm 所示的 unit -&gt; option&lt;'T&gt; 类型。

但是,您也可以使用 F# 计算表达式的灵活性,并使用不同的类型作为返回值 Delay。然后您需要相应地修改Combine 操作并实现Run 成员,但这一切都很好:

type OptionBuilder() = 
  member x.Bind(v, f) = Option.bind f v
  member x.Return(v) = Some v
  member x.Zero() = Some ()
  member x.Combine(v, f:unit -> _) = Option.bind f v
  member x.Delay(f : unit -> 'T) = f
  member x.Run(f) = f()
  member x.While(cond, f) =
    if cond() then x.Bind(f(), fun _ -> x.While(cond, f)) 
    else x.Zero()

let maybe = OptionBuilder()

诀窍是 F# 编译器在您有需要延迟的计算时使用 Delay - 即:1) 包装整个计算,2) 当您按顺序组合计算时,例如在计算中使用 if 和 3) 延迟 whilefor 的主体。

在上面的定义中,Delay 成员返回unit -&gt; M&lt;'a&gt; 而不是M&lt;'a&gt;,但这很好,因为CombineWhileunit -&gt; M&lt;'a&gt; 作为第二个参数。此外,通过添加评估函数的Run,评估maybe { .. }块(延迟函数)的结果,因为整个块被传递给Run

// As usual, the type of 'res' is 'Option<int>'
let res = maybe { 
    // The whole body is passed to `Delay` and then to `Run`
    let! a = Some 3
    let b = ref 0
    while !b < 10 do 
      let! n = Some () // This body will be delayed & passed to While
      incr b
    if a = 3 then printfn "got 3"
    else printfn "got something else"
    // Code following `if` is delayed and passed to Combine
    return a }

这是一种为非延迟类型定义计算构建器的方法,它很可能比在函数内包装类型更有效(如在 kkm 的解决方案中),并且不需要定义该类型的特殊延迟版本。

请注意,此问题不会发生在例如Haskell,因为那是一种惰性语言,所以它不需要显式地延迟计算。我认为 F# 翻译非常优雅,因为它允许处理延迟的类型(使用返回 M&lt;'a&gt;Delay)和仅表示立即结果的类型(使用返回函数的 Delay 和 @987654345 @)。

【讨论】:

  • 很好的答案。只是 Haskell 的懒惰在这里起作用吗?在 Haskell 中,while 循环甚至没有意义,因为它依赖于副作用,对吧?
  • 另外,我有点惊讶你的ZeroCombine 不像Haskell 中Maybe 的典型MonadPlus 实例,尽管我想这都是一个问题您期望的语义。
  • @kvb 关于while 循环的要点。我猜while 循环在 monad 的上下文中可能是有意义的,因为 monad 可能会提供副作用,使这种构造在 Haskell 中有用。条件函数也必须是一元的。
  • @kvb 关于CombineZero - 我认为它们出现在两个不同的上下文中。如果您的 monad 可以返回多个结果(可能使用yield 编写),您可以类似于MonadPlus 使用它们。然后Combine 组合这些值,Zero 不返回任何值。但是,它们也可以用于(如这里)副作用计算的顺序组合。这里,Zero 返回不返回的计算(在执行副作用之后),Combine 将具有副作用且不返回(第一个参数)的计算与返回值的计算结合起来。
  • 谢谢托马斯。我以为我必须遵守文档msdn.microsoft.com/en-us/library/dd233182.aspx 中的签名,但似乎这些只是作为指导,而不是严格的规则。真正重要的是所有方法都可以按照第二个表中描述的方式组合,对吧?
【解决方案2】:

根据一元身份,您的delay 应始终等价于

let delay f = bind (return ()) f

自从

val bind : M<'T> -> ('T -> M<'R>) -> M<'R>
val return : 'T -> M<'T>

delay 的签名是

val delay : (unit -> M<'R>) -> M<'R>

'T 被类型绑定到unit。请注意,您的 bind 函数的参数与习惯顺序 bind p rest 相反。这在技术上是相同的,但确实使阅读代码变得复杂。

由于您将一元类型定义为type Maybe&lt;'a&gt; = option&lt;'a&gt;,因此不会延迟计算,因为该类型根本不包含任何计算,只有一个值。因此,您将延迟定义为let delay f = f() 在理论上是正确的。但这对于 while 循环是不够的:循环的“主体”将在其“测试条件”之前计算,实际上是在绑定 bind 之前。为避免这种情况,您需要使用额外的延迟层重新定义 monad:您无需包装一个值,而是包装一个计算,该计算采用一个单位并计算该值。

type Maybe<'a> = unit -> option<'a>

let return x = fun () -> Some(x)

let fail = fun() -> None

let bind p rest =
    match p() with
    | None -> fail
    | Some r -> rest r

请注意,包裹的计算直到在 bind 函数内部才会运行,即。 e.直到 bind 的参数被绑定后才运行。

使用上面的表达式,delay 正确简化为

let delay f = fun () -> f() 

【讨论】:

  • 感谢您的回答,它证实了我的怀疑:我需要建立在延续的基础上。我仍然对forwhile 之间的差异感到惊讶。对我来说这看起来有点不一致。
  • @kkm 出于好奇,您对“延迟应始终等于...”的说法有任何参考吗?在实现 F# 计算表达式时,有一个非常好的替代定义,但如果类似的问题已经出现在其他地方,我会非常感兴趣(我猜 Haskell 没有这个问题,理论上的“monad”也没有任何延迟。 ..)
  • @TomasPetricek:是的,我正在混合理论 monad 和 F# (eager) 实现。延迟的额外标识在 Syme et al Expert F# (我在家里的旧(不是 2.0)书中的第 9 章)。我从来没有完全探索过如果违反了会发生什么,所以有点坚持它以保持安全。而且我比我更喜欢你的方法,真的!
  • @kkm 感谢您的参考!我想我的方法更“危险”,因为使用Bind(并在任何地方使用M&lt;'T&gt;类型)定义Delay是一个安全的选择。尽管我认为使用替代方法也可以。我很想看看 Expert F# 2.0 是否也这么说!
  • @kkm 我特别想知道像Delay 这样的东西是否存在于其他语言中......
猜你喜欢
  • 1970-01-01
  • 2019-01-03
  • 1970-01-01
  • 2016-08-22
  • 2013-12-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-04-08
相关资源
最近更新 更多