3

我正在尝试弄清楚如何使用计算构建器来表示一组延迟的嵌套步骤。

到目前为止,我有以下内容:

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需要调用。

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

我意识到这意味着如果你有两个嵌套的步骤,它们之间有一些“正常”代码,它需要能够读取步骤声明,然后调用它(如果这有意义的话?)。

我知道这一点,Delay并且Run是用于计算表达式的延迟执行的东西,但我不确定它们在这里是否对我有帮助,因为不幸的是它们会评估所有内容。

我很可能遗漏了一些非常明显且非常“实用”的东西,但我似乎无法让它做我想做的事。


澄清

id用于演示,它们是拼图的一部分,我想象我可能如何呈现我想要的表达的“可调用”部分。

4

2 回答 2

6

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

首先,我必须添加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到您的计算构建器中,还可以像这样实现Delayedin的缺失案例Bind

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直到它找到一个节点或叶子(折叠所有其他延迟节点),然后执行与您的代码之前所做的相同的事情。

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

于 2017-08-16T11:03:43.617 回答
4

你写了:

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

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

我对免费单子没有足够的经验,无法编写完整的教程,也无法通过分步特定的免费单子解决方案直接回答您的问题。但我可以为您指出一些可以帮助您理解它们背后的概念的资源。首先,所需的 Scott Wlaschin 链接:

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

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

其次,一组指向 Mark Seemann 博客的链接。在过去的一两个月里,Mark Seemann 一直在写关于 free-monad 风格的文章,虽然直到他写了几篇文章后我才意识到这就是他所写的内容。术语上的差异一开始可能会让你感到困惑:Scott Wlaschin 对两种可能的 AST 情况(“这是命令列表的结尾”与“在此之后还有更多命令”)使用术语“停止”和“继续”。但是这两个 free-monad 案例的传统名称是“Pure”和“Free”。恕我直言,“纯”和“免费”的名称太抽象了,我更喜欢 Scott Wlaschin 的“Stop”和“KeepGoing”名称。但是我提到这一点是为了当你在 Mark Seemann 的帖子中看到“Pure”和“Free”时,你就会知道它与 Scott Wlaschin 的海龟示例是相同的概念。

好的,解释完了,这里是 Mark Seemann 帖子的链接:

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

正如我所说,我对自由单子不太熟悉,无法为您的问题提供具体答案。但希望这些链接能给你一些背景知识,可以帮助你实现你正在寻找的东西。

于 2017-08-15T02:26:12.917 回答