部分延迟计算构建器
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" 代码,它需要能够读取步骤声明,然后调用它(如果这有意义? ).
我知道 Delay
和 Run
是在计算表达式的延迟执行中使用的东西,但我不确定它们是否对我有帮助,因为不幸的是它们会评估所有内容。
我很可能遗漏了一些非常明显和非常明显的东西 "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:
- 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# 帖子。
正如我所说,我对自由 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
直到它找到一个节点或叶(折叠所有其他延迟节点),然后执行与您的代码之前所做的相同。
我想这回答了你的问题 - 但我在这里要小心一点。我认为计算表达式作为语法糖很有用,但如果你对它们的思考超过对你实际解决的问题领域的思考,它们就会非常有害——在问题中,你没有对你的实际问题说太多.如果你这样做了,答案可能会大不相同。
我正在尝试研究如何使用计算生成器来表示一组延迟的嵌套步骤。
到目前为止我得到了以下信息:
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" 代码,它需要能够读取步骤声明,然后调用它(如果这有意义? ).
我知道 Delay
和 Run
是在计算表达式的延迟执行中使用的东西,但我不确定它们是否对我有帮助,因为不幸的是它们会评估所有内容。
我很可能遗漏了一些非常明显和非常明显的东西 "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:
- 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# 帖子。
正如我所说,我对自由 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
直到它找到一个节点或叶(折叠所有其他延迟节点),然后执行与您的代码之前所做的相同。
我想这回答了你的问题 - 但我在这里要小心一点。我认为计算表达式作为语法糖很有用,但如果你对它们的思考超过对你实际解决的问题领域的思考,它们就会非常有害——在问题中,你没有对你的实际问题说太多.如果你这样做了,答案可能会大不相同。