【发布时间】:2015-08-28 08:17:04
【问题描述】:
我正在学习 F#(来自 C++/C#),我刚刚阅读了关于延续作为在递归期间避免堆栈溢出的一种方法。一开始有点难以理解,但后来我想我已经习惯了,我喜欢这种逻辑的力量。但我想了解更多细节(并确认我理解正确)以尽可能充分利用它,所以我认为是时候与社区联系了。
为了提出我的问题,我将使用一个使用递归、lambda 和延续的简单示例。
我创建一个列表:
let ml = [1;2;3]
还有一个将列表中每个元素加倍的函数:
let double a = a * 2
然后是一个递归函数,它使用延续函数 (cont) 将双精度函数应用于列表 (wlist) 的每个元素:
let rec loop cont wlist =
match wlist with
| [] -> cont []
| x::xs -> loop ( fun acc -> cont (double x::acc) ) xs
然后我得到一个新列表,其中包含旧列表中的双倍元素:
let ml2 = loop id ml
循环函数可以泛化为接受任何映射函数,但我想保持示例简单,所以我只坚持使用双函数。
分解代码:
按照我刚刚研究的理解,循环函数中的调用是尾递归的,因为调用在主体的末尾,所以调用后不需要保存堆栈信息来恢复,避免了风险堆栈溢出 。 cont 函数是在每个循环中重新定义的 lambda,我从上面的代码中得到的方式分解如下:
let loop1 = fun acc -> id(double 1::acc)
let loop2 = fun acc -> id(double 1::(double 2::acc))
let loop3 = fun acc -> id(double 1::(double 2::(double 3::acc)))
在我的示例中,在 loop3 之后,cont 函数最终由空列表的模式匹配调用:
| [] -> cont []
这转化为调用:
loop3 []
依次翻译为:
id(double 1::(double 2::(double 3::[])))
然后:
id (2::(4::(6::[])))
然后:
[2; 4; 6]
这是我想要达到的目标。
现在的问题:
1 - 这个例子非常简单,但是对于更复杂的 lambda,很难一步一步地计算一个延续的演变来验证一个新的算法。我尝试了 vstudio 调试器,但是当我检查 continuataion 时,它只是告诉我它已在我的源代码的第 x 行定义。谢谢,但不是很有用,这不是静态 lambda,它在每个循环中都重新定义,那么循环某个点的 lambda 的当前主体是什么?我也无法调试进入 lambda。哪种工具或技巧可以为我提供这些信息并避免用我的想法来计算?我应该将 printf 放在函数中以遵循逻辑吗?风险是,如果我在匆忙的项目中工作(几乎总是如此),我会跳过这个逻辑,因为它很难验证和调试
2 - 延续避免了对堆栈充电,因此有助于避免堆栈溢出,在此示例中,可以使用具有不断增长的列表的累加器值,但不可能使用树。我的问题是,关于 lambda 的信息存储在哪里?这是某种新的和删除的分配吗?重新定义时是否会从内存中删除每个循环中的旧 lambda?
3 - 有没有什么工具可以告诉我 F# 是如何在 .net 中编译的?但不是在汇编级别,更高级别的可读性更高。我已经看到了一些示例,这些示例显示了 F# 在某种 C# 中编译和翻译( C# and F# lambda expressions code generation) 但他们没有提到工具。
感谢您阅读此消息,对不起。
【问题讨论】:
-
所以对于第 3 部分,大多数 .net 反编译器将尝试转换为 C#,根据结构的不同,C# 的可读性更高/更低。
标签: f# continuations