【发布时间】:2016-03-25 15:16:37
【问题描述】:
问题
可以通过遵循连续传递样式 (CPS) 使每个递归函数尾递归。据我了解,您将第一次递归调用之后的所有内容都放入一个函数中,并将其交给同一个调用。因此递归调用是函数中的最后一条语句,编译器能够进行尾调用优化。这意味着递归被循环替换。没有消耗额外的堆栈帧。
continuation 是一个函数,它累积所有剩余的工作。在我看来,随着每次递归调用(或循环迭代),延续性都在增长。我想知道在执行循环时,这组不断增长的指令存储在内存中的什么位置。据我所知,只有两个内存部分可以保存动态数据:堆栈和堆。我会排除堆栈,因为堆栈帧大小在已经分配时是固定的。它不能容纳不断增长的延续指令集,所以剩下堆。也许堆栈帧包含一个指向存储延续函数的内存地址的指针。这个假设正确吗?
示例
这里有一个简单的例子。这是一个非尾递归的递归函数:
// bigList: int -> int list
let rec bigList = function
| 0 -> []
| n -> 1 :: bigList (n-1)
当参数 n 较小时,一切正常:
> bigList 3;;
val it : int list = [1; 1; 1]
但是当 n 很好时,你会得到一个 stackoverflow 错误:
> bigList 170000;;
Stack overflow in unmanaged: IP: 0x2dcdb0, fault addr: 0xbf759ffc
Stack overflow in unmanaged: IP: 0x2dcdb0, fault addr: 0xbf758ffc
...
这基本上是相同的功能,但在延续传递风格:
// bigListC: int -> (int list -> 'a) -> 'a
let rec bigListC n c =
match n with
| 0 -> c []
| n -> bigListC (n-1) (fun res -> c (1::res))
你用身份函数(id)调用函数:
> bigListC 3 id;;
val it : int list = [1; 1; 1]
如您所见,它不会受到 stackoverflow 问题的影响:
> bigListC 170000 id;;
val it : int list =
[1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1;
1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1;
1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1;
1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1; 1;
...]
随着每个循环,延续性都会增长一点:
// bigListC 1 id:
> (fun res -> id (1::res)) [];;
val it : int list = [1]
// bigListC 2 id:
> (fun res -> (fun res -> id (1::res)) (1::res)) [];;
val it : int list = [1; 1]
// bigListC 3 id:
> (fun res -> (fun res -> (fun res -> id (1::res)) (1::res)) (1::res)) [];;
val it : int list = [1; 1; 1]
【问题讨论】:
标签: f# continuations