问题 为什么全懒惰是默认优化?


完全懒惰了 反复 证明原因 空间 泄漏

为什么要完全懒惰 -O 向前?我发现自己不相信SPJ的推理 函数式编程语言的实现。声称是在

f = \y -> y + sqrt 4

sqrt 4 每次不必要地重复 f 输入所以我们应该将它漂浮在lambda之外。我同意这一点,但是因为我们已经看到了这种转变导致的大问题我不相信这是值得的。在我看来,这种转换的好处可以单方面获得**只有本地代码更改,而想要它的程序员应该手动实现它。

你能说服我吗?是 full-laziness 其实真的有用吗?如果你能提供需要多边合作或非地方转型的手工实施的例子,我将特别相信。

**与内联和流融合等优化不同,手动实现需要模块之间的多边合作和非本地代码更改


8057
2018-01-31 14:52


起源

该 full-laziness transformantion实际上与评估顺序无关。 - Tom Ellis
反对完全懒惰的另一个可能点是来自 这个帖子。 - nh2


答案:


至少有一种常见的情况是完全懒惰是“安全的”和优化。

g :: Int -> Int
g z = f (z+1)
  where f 0 = 0
        f y = 1 + f (y-1)

这真的意味着 g = \z -> let {f = ...} in f (z+1) 并且,以这种方式编译,将分配一个闭包 f 在打电话之前。显然这很愚蠢,编译器应该将程序转换成

g_f 0 = 0
g_f y = 1 + g_f (y-1)
g z = g_f (z+1)

其中不需要分配呼叫 g_f。令人高兴的是,完全懒惰的转变确实如此。

显然,程序员可以避免使这些本地定义不依赖于顶级函数的参数,但这种定义通常被认为是好的风格......

另一个例子:

h :: [Int] -> [Int]
h xs = map (+1) xs

在这种情况下,你可以减少,但通常你不能减少。并命名该功能 (+1) 真是太难看了。


10
2018-01-31 15:38



谢谢你的例子。如果相同则适用 f 是一个 Int,特别是如果计算它是昂贵的。但是,我在想一个不那么暴力的转变: g = let {f = ...} in \z -> f (z+1)。我认为这仍然被认为是好的风格,即使它在语法上不适合Haskell的 where。 - Tom Ellis
你的第二个例子, h,更有说服力,因为给一个名字 (+1) 确实非常难看。我认为它等同于SPJ的例子 sqrt 4。我想我的结论是那样的 full-laziness 通过一千次切割为您免于死亡。人们原则上可以手工生产优化版本,但到处都是低效的。 - Tom Ellis
请注意,浮出lambda应该 总是 是有益的。浮动的thunk或部分应用程序可能会产生难以消除的副作用(保留重建成本低廉的庞大数据结构)。令人遗憾的是,GHC错过了一面旗帜。 - Sebastian Graf