使用最终工作流程时,缺乏尾部调用优化是否会成为障碍?
我正在使用 F# 规范中的 Final 工作流程的修改版本来进行 Xbox 开发。 Xbox 上的 .net 框架似乎不支持尾部调用。因此,我必须在编译时禁用尾调用优化。
虽然乍一看这个限制可能会阻止在计算表达式中使用任何形式的循环,但我最初认为“步进”可以避免这个问题:计算表达式中的递归函数 f 不会直接调用自身,而是它返回包含调用 f 的 lambda 的最终值。
实验表明我对 while 循环的看法是正确的(它们在计算表达式中使用时不会导致堆栈溢出),但对递归函数的看法则不然。
澄清一下,这是有效的:
// Wait until "start" or "A" is pressed on one of the gamepads.
// Set "player" when that happens.
let player : PlayerIndex option ref = ref None
while (!player).IsNone do
for p in all_players do
let state = GamePad.GetState(p)
if state.IsConnected
&& (state.Buttons.Start = ButtonState.Pressed
|| state.Buttons.A = ButtonState.Pressed) then
player := Some p
do! sys.WaitNextFrame()
这会导致堆栈溢出:
// Wait until "start" is pressed on the controlling gamepad.
let rec wait() = task {
input.Update()
if not (input.IsStartPressed()) then
do! sys.WaitNextFrame()
do! wait()
}
在第二种情况下,堆栈跟踪显示对“bind@17-1”的一长串调用,其代码如下所示。堆栈跟踪中出现的行号是第 17 行。
let rec bind k e =
match e with
| Completed r ->
Running(fun () -> k r)
| Running f ->
Running(fun () -> f() |> bind k) // line 17
| Blocked(dt, f) ->
Blocked(dt, fun () -> f() |> bind k)
| BlockedNextFrame f ->
BlockedNextFrame(fun () -> f() |> bind k)
| Yield f ->
Yield(fun () -> f() |> bind k)
我哪里错了?我关于“可步进递归”对于堆栈溢出无害的推理是否正确?我的bind实现有问题吗?
哦,我的头!递归的连续传递让我头疼......
编辑:“可步进递归对于堆栈溢出是无害的”有一个名字,我刚刚了解到。它被称为蹦床。
I'm using a modified version of the Eventually workflow from the F# spec for my development on Xbox. The .net framework on the Xbox does not support tail calls, it seems. Because of this, I have to disable tail call optimization when compiling.
Although it might seem at first that this restriction would prevent the use of any form of looping in computation expressions, I initially thought that "stepping" would avoid that problem: A recursive function f in the computation expression does not call itself directly, instead it returns an Eventually value containing a lambda which calls f.
Experiments indicate that I was right about while loops (they don't cause stack overflows when used in computation expressions), but not about recursive functions.
To clarify, this works:
// Wait until "start" or "A" is pressed on one of the gamepads.
// Set "player" when that happens.
let player : PlayerIndex option ref = ref None
while (!player).IsNone do
for p in all_players do
let state = GamePad.GetState(p)
if state.IsConnected
&& (state.Buttons.Start = ButtonState.Pressed
|| state.Buttons.A = ButtonState.Pressed) then
player := Some p
do! sys.WaitNextFrame()
This causes a stack overflow:
// Wait until "start" is pressed on the controlling gamepad.
let rec wait() = task {
input.Update()
if not (input.IsStartPressed()) then
do! sys.WaitNextFrame()
do! wait()
}
In the second case, the stack trace shows a long sequence of calls to "bind@17-1", whose code is shown below. The line number appearing in the stack trace is line 17.
let rec bind k e =
match e with
| Completed r ->
Running(fun () -> k r)
| Running f ->
Running(fun () -> f() |> bind k) // line 17
| Blocked(dt, f) ->
Blocked(dt, fun () -> f() |> bind k)
| BlockedNextFrame f ->
BlockedNextFrame(fun () -> f() |> bind k)
| Yield f ->
Yield(fun () -> f() |> bind k)
Where am I wrong? Is my reasoning about "steppable recursion" being harmless w.r.t. stack overflows incorrect? Is there something wrong with my implementation of bind?
Oh my head! Continuation-passing with recursion gives me headaches...
EDIT: "steppable recursion being harmless w.r.t. stack overflows" has got a name, I have just learned. It's called a trampoline.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
将最后一个 do! 替换为 return!:
编辑
根据 F# 规范 ,do! wait() 将转换为 Bind(wait(), fun() -> Zero()),因此每个递归调用都会增加
相反 中 的 Bind 嵌套级别(如您在堆栈跟踪中看到的)返回! wait() 将立即返回新的“最终”计算,可以在下一步中使用。
replace the last do! with return!:
EDIT
according to F# spec, do! wait() will be transformed to Bind(wait(), fun() -> Zero()), so every recursive call will increase level of Bind nesting (as you see in stacktrace)
in opposite return! wait() will immediately return new 'Eventually' computation that can be consumed on the next step.
了解正在发生的情况的一种方法是查看类型签名。
f# 中的所有函数都必须返回一些内容。因此,下面的代码
相当于
如果我们查看 Return 的定义,它会调用 result,而 result 又会返回 Completed(r)。
我为任务做了两个小测试。
通过一些仪器它可以打印。
有关计算表达式调用的 MSDN 文档。
One way to understand what's happening is to look at the type signatures.
All functions in f# have to return something. So the following code
is equivalent to
If we look at the definition of Return it calls result which in-turn returns a Completed(r).
I made two small test for task.
With some instrumentation it prints.
MSDN doc on Computation Expression calls.