我正在研究如何使用计算生成器来表示一组延迟的嵌套步骤。
到目前为止,我得到了以下内容:
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
进行演示,它们是谜题的一部分,我想象我如何展示我想要的表达式中"可调用"的部分。
正如另一个答案中所提到的,free monad为思考这类问题提供了一个有用的理论框架-然而,我认为你不一定需要它们来回答你在这里提出的特定问题。
首先,我必须将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
,直到它找到一个节点或一个叶(折叠所有其他延迟节点),然后执行与代码之前相同的操作。
我认为这回答了你的问题——但我在这里会小心一点。我认为计算表达式作为一种句法糖是有用的,但如果你对它们的思考多于对你实际解决的问题领域的思考,那么它们是非常有害的——在这个问题中,你没有对你的实际问题说太多。如果你这样做了,答案可能会大不相同。
您写道:
理想情况下,我想要的是能够检索树结构,但能够调用一些返回的函数,然后执行内部块。
这是对"自由monad"的近乎完美的描述,它基本上是OOP"解释器模式"的函数编程等价物。免费monad背后的基本思想是将命令式代码转换为两步过程。第一步构建AST,第二步执行AST。这样,您就可以在步骤1和步骤2之间做一些事情,比如在不执行代码的情况下分析树结构。然后,当您准备好时,您可以运行"execute"函数,该函数将AST作为输入,并实际执行它所代表的步骤。
我对免费monad没有足够的经验,无法编写完整的教程,也无法用一个循序渐进的特定免费monad解决方案直接回答您的问题。但我可以向您介绍一些资源,这些资源可能有助于您理解它们背后的概念。首先,所需的Scott Wlaschin链接:
https://fsharpforfunandprofit.com/posts/13-ways-of-looking-at-a-turtle-2/#way13
这是他"13种看乌龟的方式"系列的最后一部分,他使用许多不同的设计风格构建了一个类似LOGO的乌龟图形应用程序。在#13中,他使用了免费的monad风格,从头开始构建,这样你就可以看到该风格的设计决策。
第二,一组链接到Mark Seemann的博客。在过去的一两个月里,Mark Seemann一直在写关于免费monad风格的帖子,尽管我直到他在上发表了几篇文章才意识到这就是他所写的。一开始,有一个术语差异可能会让你感到困惑:Scott Wlaschin在两种可能的AST情况下使用了术语"Stop"one_answers"KeepGoing"("这是命令列表的末尾"与"这之后还有更多命令")。但这两个免费monad案例的传统名称是"Pure"one_answers"free"。IMHO,"Pure"one_answers"Free"的名字太抽象了,我更喜欢Scott Wlaschin的"Stop"one_answers"KeepGoing"的名字。但我提到这一点是为了当你在Mark Seemann的帖子中看到"Pure"one_answers"Free"时,你会知道这与Scott Wlaschin的海龟例子是一样的概念。
好吧,解释结束后,以下是Mark Seemann帖子的链接:
- http://blog.ploeh.dk/2017/06/27/pure-times/
- http://blog.ploeh.dk/2017/06/28/pure-times-in-haskell/
- http://blog.ploeh.dk/2017/07/04/pure-times-in-f/
- http://blog.ploeh.dk/2017/07/10/pure-interactions/
- http://blog.ploeh.dk/2017/07/11/hello-pure-command-line-interaction/
- http://blog.ploeh.dk/2017/07/17/a-pure-command-line-wizard/
- http://blog.ploeh.dk/2017/07/24/combining-free-monads-in-haskell/
- http://blog.ploeh.dk/2017/07/31/combining-free-monads-in-f/
- http://blog.ploeh.dk/2017/08/07/f-free-monad-recipe/
Mark将Haskell示例与F#示例穿插在一起,从URL中可以看出。如果你完全不熟悉Haskell,你可能会跳过这些帖子,因为它们可能会让你困惑,而不是帮助你。但是,如果你对Haskell语法有一点熟悉,那么看到Haskell和F#中表达的相同想法可能会帮助你更好地理解概念,所以我包括了Haskell帖子和F#帖子。
正如我所说,我对免费单声道还不够熟悉,无法为您的问题提供特定的答案。但希望这些链接能给你一些背景知识,帮助你实现你想要的东西。