在学术用途之外,continuation monad 在现实世界中是否适用?

Is there a real-world applicability for the continuation monad outside of academic use?

(后来的访客:这个问题的两个答案都提供了很好的见解,如果您有兴趣,您可能应该将它们都阅读,我只能排除一个作为SO的限制)

从我在网上找到的关于 continuation monad 的所有讨论中,他们要么提到如何将它用于一些简单的例子,要么解释说它是一个基本的构建块,如 上的这篇文章Mother of all monads is the continuation monad.

不知是否有超出此范围的适用性。我的意思是,将递归函数或相互递归包装在延续 monad 中是否有意义?它有助于提高可读性吗?

这是从 this SO post 中获取的延续模式的 F# 版本:

type ContinuationMonad() =
    member this.Bind (m, f) = fun c -> m (fun a -> f a c)
    member this.Return x = fun k -> k x

let cont = ContinuationMonad()

是否仅出于学术兴趣,例如帮助理解 monad 或计算构建器?或者是否有一些现实世界的适用性、增加的类型安全性,或者它是否规避了其他方式难以解决的典型编程问题?

即,continuation monad with call/cc from Ryan Riley 表明处理异常很复杂,但它没有解释它试图解决什么问题,而且示例也没有说明为什么它特别需要这个 monad。诚然,我只是不明白它的作用,但它可能是一个宝库!

(注意:我对理解continuation monad的工作原理不感兴趣,我认为我对它有一定的了解,我只是看不出它解决了什么编程问题。)

"mother of all monads" 内容并非纯粹的学术内容。 Dan Piponi 参考了 Andrzej Filinski 的 Representing Monads,这是一篇相当不错的论文。它的结果是如果你的语言有定界的延续(或者可以用 call/cc 和一个可变状态来模仿它们)那么你可以 透明地 添加 any 对任何代码的单子效应。换句话说,如果你有分隔的延续并且没有其他副作用,你可以实现(全局)可变状态或异常或回溯非确定性或合作并发。您可以通过定义一些简单的函数来完成这些中的每一个。没有全球转型或任何需要。此外,您只需在使用时为副作用付费。事实证明,计划者关于 call/cc 具有高度表现力的说法是完全正确的。

如果您的语言没有定界延续,您可以通过延续 monad(或更好的双管延续 monad)获得它们。当然,如果您无论如何都打算以 monadic 风格编写 – 全局转换 – 为什么不从一开始就使用所需的 monad?对于 Haskellers,这通常是我们所做的,但是,在许多情况下使用延续 monad 仍然有好处(尽管隐藏起来)。一个很好的例子是 Maybe/Option monad,它就像有异常,只是只有一种类型的异常。基本上,这个 monad 捕获返回 "error code" 并在每次函数调用后检查它的模式。这正是典型定义所做的,除了 "function call" 我的意思是计算的每个(单子)步骤。可以说,这是非常低效的,尤其是当绝大多数时间都没有错误时。但是,如果您将 Maybe 反映到延续 monad 中,虽然您必须支付 CPSed 代码的费用(GHC Haskell 处理得非常好),但您只需支付检查 "error code" 的费用重要的地方,即 catch 语句。在 Haskell 中,Codensity monad 比 danidiaz 提到的更好,因为 last Haskellers 想要的是让它产生任意效果可以在他们的代码中透明地交错。

正如 danidiaz 还提到的,许多 monad 本质上使用延续 monad 或一些变体更容易或更有效地实现。回溯搜索就是一个例子。虽然不是关于回溯的最新内容,但我最喜欢的一篇使用它的论文是 Typed Logical Variables in Haskell. The techniques used in it was also used in the Wired Hardware Description Language. Also from Koen Claesson is A Poor Man's Concurrency Monad. More modern uses of the ideas in this example include: the monad for deterministic parallelism in Haskell A Monad for Deterministic Parallelism and scalable I/O managers Combining Events And Threads For Scalable Network Services. I'm sure I can find similar techniques used in Scala. If it wasn't provided, you could use a continuation monad to implement asynchronous workflows in F#. In fact, Don Syme references exactly the same papers I just referenced. If you can serialize functions but don't have continuations, you can use a continuation monad to get them and do the serialized continuation type of web programming made popular by systems like Seaside。即使没有可序列化的延续,您也可以使用该模式(本质上与异步相同)在本地存储延续并仅发送密钥时至少避免回调。

最终,Haskeller 以外的人很少有人以任何身份使用 monad,正如我之前提到的,Haskeller 倾向于使用比 continuation monad 更可控的 monad ,尽管他们确实在内部使用了很多。尽管如此,continuation monad 或 continuation monad 之类的东西,特别是对于异步编程,正变得越来越不常见。随着 C#、F#、Scala、Swift,甚至 Java 开始合并支持 monadic 或至少是 monadic 风格的编程,这些想法将得到更广泛的应用。如果 Node 开发人员更熟悉这一点,也许他们会意识到在事件驱动编程方面您也可以吃蛋糕。

为了提供更直接的特定于 F# 的答案(即使 Derek 也已经涵盖了这一点),continuation monad 几乎抓住了 asynchronous 的核心工作流程 有效。

continuation monad 是一个函数,当给定一个 continuation 时,它最终会调用带有结果的 continuation(它可能永远不会调用它,也可能会重复调用它):

type Cont<'T> = ('T -> unit) -> unit

F# 异步计算有点复杂 - 它们采用延续(在成功的情况下)、异常和取消延续,并且还包括取消标记。使用稍微简化的定义,F# 核心库使用(参见 the full definition here):

type AsyncParams =
    { token : CancellationToken
      econt : exn -> unit
      ccont : exn -> unit } 

type Async<'T> = ('T -> unit) * AsyncParams -> unit

如你所见,如果忽略AsyncParams,它几乎就是continuation monad。在 F# 中,我认为 "classical" monad 作为灵感比作为直接实现机制更有用。在这里,continuation monad 提供了一个有用的模型来说明如何处理某些类型的计算 - 以及许多额外的特定于异步的方面,核心思想可用于实现异步计算。

我认为这与 monad 在经典学术著作或 Haskell 中的使用方式有很大不同,后者倾向于使用 "as is" 并且可能以各种方式组合以构建更复杂的 monad捕获更复杂的行为。

这可能只是我个人的看法,但我要说的是,continuation monad 本身并没有实际用处,但它是一些非常实用的想法的基础。 (就像 lambda 演算本身并没有实际用处,但它可以被视为对实用语言的启发!)

我当然发现与使用显式递归实现的递归函数相比,使用延续 monad 实现的递归函数更容易阅读。例如,给定此树类型:

type 'a Tree = 
| Node of 'a * 'a Tree * 'a Tree
| Empty

这是在树上写自下而上折叠的一种方法:

let rec fold e f t = cont {
    match t with
    | Node(a,t1,t2) ->
        let! r1 = fold e f t1
        let! r2 = fold e f t2
        return f a r1 r2
    | Empty -> return e
}

这显然类似于天真的折叠:

let rec fold e f t =
    match t with
    | Node(a,t1,t2) ->
        let r1 = fold e f t1
        let r2 = fold e f t2
        f a r1 r2
    | Empty -> return e

除了在深树上调用时,naïve fold 会炸毁堆栈,因为它不是尾递归的,而使用 continuation monad 编写的 fold 不会。你当然可以使用显式延续来写同样的东西,但在我看来,它们添加的混乱数量会分散算法结构的注意力(并且将它们放在适当的位置并不是完全安全的):

let rec fold e f t k = 
    match t with
    | Node(a,t1,t2) -> 
        fold e f t1 (fun r1 ->
        fold e f t2 (fun r2 ->
        k (f r1 r2)))
    | Empty -> k e

请注意,为了使其正常工作,您需要修改 ContinuationMonad 的定义以包含

member this.Delay f v = f () v