在continuation

时间:2015-06-12 10:40:00

标签: f# continuations

我正在学习F#(来自C ++ / C#),我只是读取了继续作为在递归过程中避免堆栈溢出的方法之一。在一开始它有点难以掌握,但后来我想我已经习惯了它,我喜欢这种逻辑的力量。但我希望得到更多细节(并确认我理解正确),以便最好地利用它,所以我认为是时候与社区联系了。

要提出我的问题,我将使用递归,lambda和continuation的简单示例。

我创建了一个列表:

let ml = [1;2;3]

和一个将列表中每个元素加倍的函数:

let double a = a * 2

然后是一个递归函数,它使用连续函数(cont)将double函数应用于列表的每个元素(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的当前主体是什么?此外,我不能调试stepinto lambda。哪个工具或技巧可以给我这个信息,避免用我的想法来解决它?我应该将printf放在函数内部以遵循逻辑吗?风险是如果我在一个项目上匆忙工作(几乎总是如此)我跳过这个逻辑,因为它难以验证和调试

2 - 继续避免对堆栈充电,因此有助于避免堆栈溢出,在此示例中,可以使用具有增长列表的累加器值,但使用不可能的树。我的问题是,存储有关lambda的信息?这是某种新的和删除分配?重新划分时,每个循环中的旧lambda是否从内存中删除了?

3 - 是否有任何工具可以告诉我如何在.net中编译F#?但不是在汇编级别,更高级别的可读性更高。我看过一些示例,显示F#在某种C#中编译和翻译( C# and F# lambda expressions code generation) 但他们没有提到这些工具。

感谢您阅读此邮件,对不起,请继续阅读。

1 个答案:

答案 0 :(得分:2)

对于1,您总是可以首先以正常的堆栈分配方式编写代码(存在堆栈溢出的风险),并确保它是正确的,稍后再添加延续。这样你就可以使代码本身正确,只需添加延续就应该是一个相当直接的过程。

对于2,中间状态在堆上分配而不是堆栈。而且由于堆大得多,所以不会出现堆栈溢出,但最坏的情况是内存不足。后者是不太可能的,除非你存储一个大规模的中间状态。 lambda将作为闭包存储在堆上,而在这种情况下,闭包将是一个包含已经传递给函数的捕获值的对象。

对于3,您可以从JetBrains中查看一些像dotPeek这样的反编译器,但是您可能找不到所有可读代码。在F#中,在大多数情况下,根据区分联合(DU),记录,元组和函数进行思考,而不是实现它们的类和对象。特别是如果你看一下在F#中看起来非常相似的案例但是有不同的含义,你可能得到非常不同的C#代码,比如说:let value = fun () -> 1而不是let func () = 1。它们看起来非常相似,但实际上第一个是常量函数值,而后者是函数。特别是,在第一种情况下返回函数值之前可以进行一些繁重的计算,但不能在第二种情况下返回。方式,我喜欢理解和思考代码的方式是,我让自己意识到什么时候评估什么部分。