为什么没有在各处使用惰性评估?


32

我刚刚了解了惰性评估的工作原理,我想知道:为什么惰性评估没有应用于当前生产的每个软件中?为什么仍使用急切评估?


2
这是混合使用可变状态和惰性评估会发生什么的示例。alicebobandmallory.com/articles/2011/01/01/...
乔纳斯Elfström

2
@JonasElfström:请不要将可变状态与其可能的实现之一混淆。可以使用无限的惰性值流来实现可变状态。这样就没有可变变量的问题。
Giorgio 2015年

在命令式编程语言中,“惰性评估”需要程序员有意识的努力。使用命令式语言进行泛型编程已使此操作变得容易,但是它永远不会透明。问题另一侧的答案又引出了另一个问题:“为什么不在各处都使用函数式编程语言?”,而就当前事务而言,当前的答案仅仅是“否”。
rwong 2015年

2
并不是到处都使用函数式编程语言,这是因为我们没有在锤子上敲打锤子,同样的原因也不是每个问题都可以用函数输入->输出方式轻松表达,例如GUI更适合以命令式表达。
ALXGTV 2015年

此外,还有两类功能性编程语言(或至少两者都声称是功能性的),命令性功能性语言(例如Clojure,Scala)和声明性功能性语言(例如Haskell,OCaml)。
ALXGTV 2015年

Answers:


38

懒惰评估需要记账本钱-您必须知道它是否已经评估过,诸如此类。总是会评估急切的评估,因此您不必知道。在并发上下文中尤其如此。

其次,它的琐碎通过打包成一个函数对象急于评价转化为懒惰评估稍后调用,如果你愿意的话。

第三,懒惰的评估意味着失去控制。如果我懒惰地评估从磁盘读取文件怎么办?还是抽时间?那是不可接受的。

急切的评估可以更有效,更可控,并且可以轻松地转换为惰性评估。为什么要进行懒惰评估?


10
懒洋洋地从磁盘读取一个文件实际上是很整洁-对于大多数我简单的程序和脚本,Haskell的readFile正是我所需要的。此外,从懒惰的评估转换为热切的评估也是微不足道的。
Tikhon Jelvis '12

3
同意除最后一段以外的所有人。进行连锁操作时,惰性评估更有效,并且可以更好地控制您何时实际需要数据
texasbruce 2015年

4
函子法想与您谈谈“失控”。如果编写对不可变数据类型进行操作的纯函数,那么惰性求值是天赐之物。诸如haskell之类的语言从根本上基于懒惰的概念。在某些语言中,这很麻烦,尤其是在与“不安全”代码混合使用时,但是您使它听起来像是默认情况下懒惰是危险的或有害的。在危险代码中它只是“危险的”。
2016年

1
@DeadMG如果您不关心代码是否终止,那会不会是... head [1 ..]用一种热切评估的纯语言会给您带来1什么,因为在Haskell中它能给您带来什么?
分号

1
对于许多语言,实施惰性评估至少会引入复杂性。有时,这是很复杂的,并且进行惰性评估可以提高整体效率-尤其是在仅有条件地需要评估的情况下。但是,做得不好,由于编写代码时的错误假设,可能会引入细微的错误或难以解释性能问题。需要权衡。
Berin Loritsch

17

主要是因为懒惰的代码和状态可能混合得很糟,并导致一些难以发现的错误。如果从属对象的状态发生变化,则在评估时,惰性对象的值可能是错误的。最好让程序员在知道合适的情况下将对象显式地编写为懒惰对象。

另外,Haskell对所有内容都使用了惰性评估。这是可能的,因为它是一种功能性语言,并且不使用状态(除非在一些特殊情况下清楚地标记了它们)


是的,可变状态+懒惰的评估=死亡。我认为我在SICP决赛中失去的唯一要点是关于set!在惰性方案解释器中使用。> :(
Tikhon Jelvis 2011年

3
“惰性代码和状态可能混合得很糟”:这实际上取决于实现状态的方式。如果使用共享的可变变量来实现它,并且依赖于状态的求值顺序,那么您是对的。
Giorgio 2015年

14

惰性评估并不总是更好。

惰性评估的性能优势可能很大,但是在急切的环境中避免大多数不必要的评估并非难事-惰性使得它容易且完整,但是在代码中不必要的评估很少是主要问题。

惰性评估的好处在于,它可以让您编写更清晰的代码。通过过滤无限自然数列表获得第10个素数并采用该列表的第10个元素是进行过程中最简洁明了的方法之一:(伪代码)

let numbers = [1,2...]
fun is_prime x = none (map (y-> x mod y == 0) [2..x-1])
let primes = filter is_prime numbers
let tenth_prime = first (take primes 10)

我相信,如果没有懒惰,那么简洁地表达事情将非常困难。

但是懒惰并不是一切的答案。首先,懒惰不能在状态存在时透明地应用,并且我相信不能自动检测到状态(除非您在状态非常明确的情况下使用Haskell进行工作)。因此,在大多数语言中,懒惰需要手动完成,这使事情变得不太清楚,从而消除了懒惰评估的一大好处。

此外,懒惰在性能上有缺陷,因为它会导致保持未求值的表达式的开销很大。它们会耗尽存储空间,并且比简单的值要慢。发现您必须热切地编写代码是很平常的,因为懒惰的版本运行缓慢,并且有时很难对性能进行推理。

随着它的发生,没有绝对的最佳策略。如果您可以利用无限的数据结构或它允许使用的其他策略编写更好的代码,那么懒惰是很棒的选择,但是渴望可以更轻松地进行优化。


一个真正聪明的编译器有可能显着减轻开销。甚至利用懒惰进行额外的优化?
Tikhon Jelvis '12

3

这是对渴望和懒惰的评估的利弊的简短比较:

  • 渴望评估:

    • 不必要地评估内容的潜在开销。

    • 不受阻碍,快速评估。

  • 惰性评估:

    • 没有不必要的评估。

    • 每次使用值时的簿记开销。

因此,如果您有许多不必求值的表达式,则懒惰会更好;但是,如果您从来没有一个不需要计算的表达式,那么惰性就是纯粹的开销。

现在,让我们来看看真实的世界软件:有多少的功能,你写你不能要求他们所有的论据评价?特别是对于仅做一件事的现代短函数而言,属于此类的函数百分比非常低。因此,懒惰的评估在大多数情况下只会引入簿记开销,而没有机会真正节省任何费用。

因此,懒惰的评估根本无法平均地付出代价,热切的评估更适合现代代码。


1
“每次使用值时的簿记开销。”:我认为簿记开销并不比检查Java之类的空引用大。在这两种情况下,您都需要检查一点信息(评估/待处理与空/非空),并且每次使用值时都需要进行检查。因此,是的,这有开销,但是它是最小的。
Giorgio 2015年

1
“您编写的多少个函数不需要评估其所有参数?”:这只是一个示例应用程序。递归的,无限的数据结构呢?您可以通过急切的评估来实施它们吗?您可以使用迭代器,但是解决方案并不总是那么简洁。当然,您可能不会错过从未有机会广泛使用的东西。
Giorgio 2015年

2
“因此,懒惰的评估根本无法平均支付,急切的评估更适合现代代码。”:这句话并不成立:它实际上取决于您要实现的内容。
Giorgio

1
@Giorgio在您看来,开销似乎并不多,但是条件条件是现代CPU所吸引的东西之一:分支预测错误通常会强制执行完整的管道刷新,从而浪费了十多个CPU周期的工作。您不希望内部循环中出现不必要的条件。对于性能敏感的代码,每个函数参数额外支付十个周期几乎与在Java中编写事物一样不可接受。正确的评估是正确的,懒惰的评估使您可以提出一些急切的评估无法轻松完成的技巧。但是绝大多数代码不需要这些技巧。
cmaster

2
这似乎是对缺乏懒惰评估语言经验的答案。例如,无限数据结构呢?
Andres F.

3

正如@DeadMG指出的那样,惰性评估需要簿记开销。相对于急切的评估,这可能是昂贵的。考虑以下语句:

i = (243 * 414 + 6562 / 435.0 ) ^ 0.5 ** 3

这将需要一些计算才能计算出来。如果我使用惰性评估,则每次使用时都需要检查它是否已经评估过。如果这是在频繁使用的紧密循环内,则开销会显着增加,但没有任何好处。

有了急切的评估和良好的编译器,您可以在编译时计算公式。如果合适,大多数优化器会将分配移出发生的任何循环。

惰性评估最适合加载不经常访问的数据,并且检索开销很大。因此,与核心功能相比,它更适合边缘情况。

通常,最好的做法是尽早评估经常访问的内容。惰性评估不适用于这种做法。如果您将始终访问某些内容,那么所有惰性评估都将增加开销。随着要访问的项目变得不太可能被访问,使用惰性评估的成本/收益降低。

始终使用惰性评估还意味着尽早优化。这是一种不好的做法,通常会导致代码变得更加复杂和昂贵,否则情况可能会如此。不幸的是,过早的优化通常会使代码的执行速度比简单的代码慢。在无法衡量优化效果之前,优化代码是一个坏主意。

避免过早的优化不会与良好的编码习惯冲突。如果未应用良好实践,则初始优化可能包括应用良好的编码实践,例如将计算移出循环。


1
您似乎出于经验不足而争论不休。我建议您阅读Wadler撰写的论文“ Why Functional Programming Matters”。它的主要部分解释了为什么进行延迟评估(提示:与性能,早期优化或“加载不经常访问的数据”以及与模块化有关的一切都没有关系)。
Andres F.

@AndresF我已经阅读了您引用的论文。我同意在这种情况下使用惰性评估。早期评估可能不合适,但我认为,如果可以轻松添加其他动作,则为选定动作返回子树可能会带来很大的好处。但是,构建该功能可能是过早的优化。在函数式编程之外,我在使用懒惰评估和使用懒惰评估的失败方面有很多重大问题。有报告说,由于函数式编程中的惰性评估而导致大量性能开销。
BillThor

2
如?有报告称,在使用急切评估时也会产生巨大的执行成本(以不必要的评估以及计划未终止的形式出现的成本)。想想看,几乎所有其他(误用)功能都有成本。模块化本身可能要付出代价。问题是它是否值得。
Andres F.

3

如果我们可能必须完全评估表达式来确定其值,那么延迟评估可能是不利的。假设我们有很长的布尔值列表,并且我们想找出它们是否全部为真:

[True, True, True, ... False]

为了做到这一点,无论如何,我们都必须查看列表中的每个元素,因此不可能延迟评估。我们可以使用折叠来确定列表中的所有布尔值是否为真。如果我们使用了使用惰性求值的对折权,那么我们就无法获得惰性求值的任何好处,因为我们必须查看列表中的每个元素:

foldr (&&) True [True, True, True, ... False] 
> 0.27 secs

在这种情况下,向右折叠比不向左折叠的严格折叠要慢得多,后者不使用惰性计算:

foldl' (&&) True [True, True, True, ... False] 
> 0.09 secs

原因是使用尾部递归严格折左,这意味着它会累加返回值,并且不会建立并在内存中存储大量操作链。这比懒惰折叠要快得多,因为这两个函数无论如何都必须查看整个列表,并且折叠右不能使用尾部递归。因此,重点是,您应该使用最适合手头任务的方法。


“所以,重点是,您应该使用最适合手头任务的工具。” +1
乔治
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.