部分延迟计算构建器

Partially deferred computation builder

我正在尝试研究如何使用计算生成器来表示一组延迟的嵌套步骤。

到目前为止我得到了以下信息:

type Entry =
    | Leaf of string * (unit -> unit)
    | Node of string * Entry list * (unit -> unit)

type StepBuilder(desc:string) =
    member this.Zero() = Leaf(desc,id)
    member this.Bind(v:string, f:unit->string) =
        Node(f(), [Leaf(v,id)], id)
    member this.Bind(v:Entry, f:unit->Entry) =
        match f() with
        | Node(label,children,a) -> Node(label, v :: children, a)
        | Leaf(label,a) -> Node(label, [v], a)


let step desc = StepBuilder(desc)

let a = step "a" {
    do! step "b" {
        do! step "c" {
            do! step "c.1" {
                // todo: this still evals as it goes; need to find a way to defer
                // the inner contents...
                printfn "TEST"
            }
        }
    }
    do! step "d" {
        printfn "d"
    }
}

这会生成所需的结构:

A(B(C(c.1)), D)

我的问题是,在构建结构时,会进行 printfn 调用。

理想情况下,我想要的是能够检索树结构,但能够调用一些返回的 function/s 然后执行内部块。

我知道这意味着如果你有两个嵌套的步骤,它们之间有一些 "normal" 代码,它需要能够读取步骤声明,然后调用它(如果这有意义? ).

我知道 DelayRun 是在计算表达式的延迟执行中使用的东西,但我不确定它们是否对我有帮助,因为不幸的是它们会评估所有内容。

我很可能遗漏了一些非常明显和非常明显的东西 "functional-y" 但我似乎无法让它做我想做的事。


澄清

我正在使用 id 进行演示,它们是拼图的一部分,我想像我可能会如何表达我想要的 "invokable" 部分。

您写道:

Ideally what I want is to be able to retrieve the tree structure, but be able to call some returned function/s that will then execute the inner blocks.

这是对 "free monad" 的近乎完美的描述,它基本上是 OOP "interpreter pattern" 的函数式编程等价物。 free monad 背后的基本思想是将命令式代码转换为两步过程。第一步构建一个 AST,第二步执行 AST。这样你就可以在步骤 1 和步骤 2 之间做一些事情,比如在不执行代码的情况下分析树结构。然后,当您准备就绪时,您可以 运行 您的 "execute" 函数,它将 AST 作为输入并实际执行它所代表的步骤。

我对免费的 monad 没有足够的经验,无法编写关于它们的完整教程,也无法直接用 specific free 的步骤直接回答您的问题-monad 解决方案。但我可以为您指出一些资源,这些资源可以帮助您理解它们背后的概念。首先,所需的 Scott Wlaschin link:

https://fsharpforfunandprofit.com/posts/13-ways-of-looking-at-a-turtle-2/#way13

这是他的“看海龟的 13 种方式”系列的最后一部分,他在其中使用许多不同的设计风格构建了一个类似 LOGO 的小海龟图形应用程序。在 #13 中,他使用 free-monad 风格,从头开始构建它,因此您可以看到进入该风格的设计决策。

其次,一组 link 到 Mark Seemann 的博客。在过去的一两个月里,Mark Seemann 一直在写关于 free-monad 风格的文章,尽管直到他发表了几篇文章我才意识到这是他在写的内容。术语上的差异一开始可能会让您感到困惑:Scott Wlaschin 使用术语 "Stop" 和 "KeepGoing" 来表示两种可能的 AST 情况("this is the end of the command list" 与 "there are more commands after this one")。但是这两个自由单子案例的 传统 名称是 "Pure" 和 "Free"。恕我直言,名字 "Pure" 和 "Free" 太抽象了 ,我更喜欢 Scott Wlaschin 的 "Stop" 和 "KeepGoing" 名字。但我提到这一点是为了当您在 Mark Seemann 的帖子中看到 "Pure" 和 "Free" 时,您会知道它与 Scott Wlaschin 的乌龟示例是相同的概念。

好的,解释完了,下面是 Mark Seemann 帖子的link:

Mark 将 Haskell 示例与 F# 示例穿插在一起,您可以从 URL 中看出这一点。如果您完全不熟悉 Haskell,您可以跳过这些帖子,因为它们可能会让您感到困惑而不是帮助。但是,如果您对 Haskell 语法有一定的了解,那么在 Haskell 和 F# 中看到相同的想法可能会帮助您更好地掌握这些概念,因此我将 Haskell帖子以及 F# 帖子。

正如我所说,我对自由 monad 还不够熟悉,无法为您的问题提供 特定的 答案。但希望这些 link 能为您提供一些背景知识,帮助您实现您正在寻找的东西。

正如在另一个答案中提到的,自由单子为思考这类问题提供了一个有用的理论框架——但是,我认为你不一定需要它们来获得一个回答您在这里提出的具体问题。

首先,我必须将 Return 添加到您的计算构建器中以使您的代码编译。因为你从来没有 return 任何东西,我只是添加了一个重载 unit 相当于 Zero:

member this.Return( () ) = this.Zero()

现在,回答你的问题 - 我认为你需要修改你的区分联合以允许延迟产生 Entry 的计算 - 你在域模型中确实有函数 unit -> unit,但那是不足以延迟将产生新条目的计算。所以,我认为你需要扩展类型:

type Entry =
  | Leaf of string * (unit -> unit)
  | Node of string * Entry list * (unit -> unit)
  | Delayed of (unit -> Entry)

当您评估 Entry 时,您现在需要处理 Delayed 案例 - 它包含一个可能执行副作用的函数,例如打印 "TEST".

现在您可以将 Delay 添加到您的计算构建器中,并且还可以像这样在 Bind 中实现 Delayed 的缺失案例:

member this.Delay(f) = Delayed(f)
member this.Bind(v:Entry, f:unit->Entry) = Delayed(fun () ->
    let rec loop = function
      | Delayed f -> loop (f())
      | Node(label,children,a) -> Node(label, v :: children, a)
      | Leaf(label,a) -> Node(label, [v], a)
    loop (f()) )

本质上,Bind 将创建一个新的延迟计算,当调用时,计算条目 v 直到它找到一个节点或叶(折叠所有其他延迟节点),然后执行与您的代码之前所做的相同。

我想这回答了你的问题 - 但我在这里要小心一点。我认为计算表达式作为语法糖很有用,但如果你对它们的思考超过对你实际解决的问题领域的思考,它们就会非常有害——在问题中,你没有对你的实际问题说太多.如果你这样做了,答案可能会大不相同。