【问题标题】:Debugging lambda in continuations继续调试 lambda
【发布时间】: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


【解决方案1】:

至于 1,您始终可以首先以正常的堆栈分配方式编写代码(存在堆栈溢出的风险)并确保它是正确的,然后再添加延续。这样一来,您就可以使代码本身正确,并且之后添加延续应该是一个相当简单的过程。

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

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

【讨论】:

  • 谢谢丹尼尔,不过我同意大部分的观点:是的,我遵循模式开始简单并稍后优化,但 lambda 可以在正常堆栈递归、尾递归和延续之间切换。我仍然希望调试器可以在不断改变其主体的 lambda 上做更多的事情。听说VS2015有一些改进,不知道能不能应用到这个。我再看看吧。现在我正在继续我的学习。现在关于计算表达式,非常有趣,但是,我的第一印象,这里也缺少遵循 C.E. 的程序流程并显示细节的调试器。谢谢,也许你在苏黎世 F# 聚会;)
  • C. E. 只是 CPS 的语法糖...所以您很可能会遇到同样的问题... ;-) 但是,你一定会在 F# 聚会上见到你 ;-)
  • 除非你想见面喝杯咖啡或饮料 ;-)
  • 你是对的! CE 也有同样的问题,甚至更多是因为我不仅要处理延续,还要处理将 CE 关键字(让!、返回......)翻译成 CEbuilder 方法(绑定、归零、组合......)。我觉得调试器可以做更多,但也许只是我缺乏经验,2天......我已经很惊讶我能掌握它! :)
  • 当然,如果因为 F# 聚会或其他原因来到您所在的地区(我现在在提契诺州),我会给您留言。谢谢,祝你有美好的一天;)
猜你喜欢
  • 2020-04-14
  • 1970-01-01
  • 1970-01-01
  • 2013-09-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-08-19
相关资源
最近更新 更多