惰性评估的概念为何有用?


30

表达式的惰性求值似乎会使程序员失去对代码执行顺序的控制。我很难理解为什么程序员会接受或期望这样做。

当我们无法保证将在何时何地评估表达式时,该范式如何用于构建可预期运行的可预测软件?


10
在大多数情况下,这并不重要。对于其他所有人,您可以强制严格执行。
Cat Plus Plus

22
haskell这样的纯函数式语言的要点是,您无需在运行代码时就去打扰,因为它是无副作用的。
bitmask 2012年

21
您需要停止考虑“执行代码”,而开始考虑“计算结果”,因为这正是您在最有趣的问题中真正想要的。当然,程序通常也需要以某种方式与环境进行交互,但是通常可以将其减少为代码的一小部分。其余部分,您可以纯粹地工作,并且懒惰可以使推理变得简单得多。
大约

6
标题中的问题(“为什么使用惰性评估?”)与正文中的问题(“您如何使用惰性评估?”)有很大不同。对于前者,请参阅我对这个相关问题的回答
丹尼尔·瓦格纳

1
懒惰有用的一个示例:在Haskell head . sort中,O(n)由于懒惰(而不是O(n log n))而具有复杂性。请参阅惰性评估和时间复杂性
PetrPudlák2012年

Answers:


62

许多答案都涉及到无限列表和未计算部分的性能提升等问题,但这却缺少了懒惰的更大动机:模块化

约翰·休斯(John Hughes )广为引用的论文“为什么要进行功能编程”(PDF链接)中阐述了这一经典论点。该论文(第5节)中的关键示例是使用alpha-beta搜索算法播放井字游戏。关键是(第9页):

[惰性评估]使得将程序模块化为实用工具,使其能够生成大量可能的答案以及选择适当答案的选择器。

Tic-Tac-Toe程序可以编写为从给定位置开始生成整个游戏树的函数,也可以编写为消耗该函数的单独函数。在运行时,这不会本质上生成整个游戏树,而只会生成消费者实际需要的那些子部分。我们可以通过改变消费者来改变生产替代品的顺序和组合。根本不需要更换发电机。

用一种急切的语言,您不能以这种方式编写它,因为您可能会花费太多时间和内存来生成树。因此,您最终要么:

  1. 将产生和消耗合并为相同的功能;
  2. 写出只对某些消费者最佳的生产者;
  3. 实现自己的懒惰版本。

请更多信息或示例。这听起来很有趣。
亚历克斯·奈

1
@AlexNye:John Hughes的论文有更多信息。尽管是学术论文-毫无疑问是令人生畏的-但它实际上是非常易于理解的。如果不是它的长度,它可能适合作为答案!
Tikhon Jelvis

也许要理解这个答案,就必须阅读休斯的论文……还没读完,我仍然看不到懒惰和模块化之间的关系以及原因。
stakx 2014年

@stakx如果没有更好的描述,它们似乎没有偶然的联系。在此示例中,惰性的优势在于,惰性生成器能够生成游戏的所有可能状态,但是这样做不会浪费时间/内存,因为只会消耗发生的时间/内存。生成器可以与使用者分离,而不必成为惰性生成器,并且有可能(尽管更困难)在不与使用者分离的情况下成为惰性。
2014年

@Iztaka:“生成器可以与消费者分离,而不必成为懒惰的生成器,并且有可能(尽管更困难)在不与消费者分离的情况下变得懒惰。” 请注意,当您走这条路线时,您可能会得到**过度专业化的生成器** —生成该生成器是为了优化一个使用方,而当将其重用于其他使用方时,它是次优的。一个常见的示例是对象关系映射器,它们仅由于您要从根对象获得一个字符串而获取并构造整个对象图。懒惰避免了很多这样的情况(但不是全部)。
sacundim

32

当我们无法保证将在何时何地评估表达式时,该范式如何用于构建可预期运行的可预测软件?

当表达式没有副作用时,表达式的计算顺序不会影响其值,因此程序的行为不受该顺序的影响。因此,行为是完全可以预测的。

现在副作用是另一回事了。如果副作用可能以任何顺序发生,则该程序的行为确实是不可预测的。但这实际上并非如此。像Haskell这样的惰性语言很重要,它具有参照透明性,即确保表达式的求值顺序不会影响其结果。在Haskell中,这是通过强制所有具有用户可见副作用的操作在IO monad内部进行来实现的。这样可以确保所有副作用均按您期望的顺序发生。


15
这就是为什么默认情况下,只有像Haskell这样具有“强制性纯净”的语言才支持惰性的原因。诸如Scala之类的“鼓励纯净”语言需要程序员明确地说出他们想要懒惰的地方,这取决于程序员确保懒惰不会引起问题。一种默认情况下具有惰性并且具有无法跟踪的副作用的语言确实很难以可预测的方式进行编程。
2012年

1
当然,除IO以外的monad也会引起副作用
jk。

1
@jk只有IO会导致外部副作用。
dave4420 '09年

@ dave4420是的,但不是这个答案说的话
JK。

1
@jk在Haskell中实际上没有。除IO(或基于IO构建的IO)之外,没有monad会产生副作用。这仅仅是因为编译器对待IO的方式有所不同。它认为IO是“不可改变的”。Monad只是确保特定执行顺序的一种(巧妙的)方法(因此,只有在用户输入“是” 之后,您的文件才会被删除)。
Scarridge

22

如果您熟悉数据库,则处理数据的一种非常常见的方法是:

  • 提出类似的问题 select * from foobar
  • 当有更多数据时,请执行以下操作:获取下一行结果并进行处理

您无法控制结果的生成方式以及生成索引的方式(索引,全表扫描吗?)或生成时间(何时要求生成所有数据?您所知道的是:如果有更多数据,您将在需要时得到它。

惰性评估非常接近同一件事。假设您有一个定义为ie的无限列表。斐波那契数列-如果您需要五个数字,则可以计算五个数字;如果需要1000,则会得到1000。诀窍是运行时知道何时何地提供什么。非常方便。

(Java程序员可以使用Iterators模仿这种行为-其他语言可能具有类似的功能)


好点子。例如Collection2.filter()(以及该类中的其他方法)几乎实现了惰性求值:结果“看起来”很普通Collection,但是执行的顺序可能是不直观的(或至少不是显而易见的)。另外,yieldPython中有一个(C#中有一个类似的功能,我不记得它的名字),它比普通的Iterator更接近于支持懒惰求值。
约阿希姆·绍尔

@JoachimSauer在C#中可以产生收益,或者您当然可以使用prebuild linq优化器,其中约有一半是懒惰的
jk。

+1:用于以命令式/面向对象的语言提及迭代器。我使用类似的解决方案在Java中实现流和流功能。使用迭代器,我可以在长度未知的输入流上使用类似take(n),dropWhile()之类的函数。
Giorgio 2012年

13

考虑向数据库询问名称以“ Ab”开头且年龄超过20年的前2000个用户的列表。他们也必须是男性。

这是一张小图。

You                                            Program Processor
------------------------------------------------------------------------------
Get the first 2000 users ---------->---------- OK!
                         --------------------- So I'll go get those records...
WAIT! Also, they have to ---------->---------- Gotcha!
start with "Ab"
                         --------------------- NOW I'll get them...
WAIT! Make sure they're  ---------->---------- Good idea Boss!
over 20!
                         --------------------- Let's go then...
And one more thing! Make ---------->---------- Anything else? Ugh!
sure they're male!

No that is all. :(       ---------->---------- FINE! Getting records!

                         --------------------- Here you go. 
Thanks Postgres, you're  ---------->----------  ...
my only friend.

从这种可怕的可怕交互中可以看出,“数据库”在准备好处理所有条件之前并没有做任何事情。它是每个步骤的延迟加载结果,并且每次都应用新条件。

与获得最初的2000个用户相反,返回它们,过滤它们以“ Ab”,返回它们,过滤它们20多个,返回它们并过滤男性并最终返回它们。

延迟加载。


1
这真是糟糕的解释恕我直言。不幸的是,我在这个特定的SE网站上没有足够的代表来拒绝它。延迟评估的真正意义在于,除非准备好使用这些结果,否则实际上不会产生任何结果。
Alnitak

我发布的答案与您的评论完全相同。
sergserg 2012年

那是一个非常有礼貌的程序处理器。
朱利安

9

表达式的延迟求值将导致给定代码段的设计者失去对其代码执行顺序的控制。

如果结果相同,设计人员就不必在乎表达式的计算顺序。通过推迟评估,可以避免完全评估某些表达式,从而节省了时间。

您可以在较低的层次上看到相同的想法:许多微处理器能够无序地执行指令,这使它们可以更有效地使用各种执行单元。关键是他们要看指令之间的依赖性,并避免在可能改变结果的地方重新排序。


5

我认为有很多关于懒惰评估的论点

  1. 模块化通过惰性评估,您可以将代码分解为多个部分。例如,假设您遇到“在列表中查找元素的前十个倒数,使倒数小于1”的问题。在类似Haskell的东西中,您可以编写

    take 10 . filter (<1) . map (1/)
    

    但这在严格的语言中是不正确的,因为如果您给出它[2,3,4,5,6,7,8,9,10,11,12,0],则将被零除。请参阅sacundim的答案,以了解为什么这在实践中很棒

  2. 更多事情起作用严格(双关语意)更多程序终止于非严格评估而不是严格评估。如果您的程序以“急切”的评估策略终止,则它将以“懒惰”的评估策略终止,但是相反的说法是不正确的。您会得到诸如无限数据结构(实际上只是很酷)之类的东西作为这种现象的具体示例。更多程序可以使用惰性语言。

  3. 最优性按需 调用评估相对于时间是渐近最优的。尽管主要的惰性语言(本质上是Haskell和Haskell)不保证按需致电,但您可以或多或少地期望最佳成本模型。严格的分析器(和投机评估)在实践中降低了开销。空间是一个更复杂的问题。

  4. 使用惰性评估来实现“ 强制纯净”会使您无序地应对副作用,因为如您所说,程序员会失去控制。这是一件好事。参照透明性使对程序的编程,验证和推理变得非常容易。严格的语言不可避免地会屈服于具有不纯净的位的压力-Haskell和Clean曾对此进行了漂亮的抵抗。这并不是说副作用总是有害的,但是控制副作用是如此有用,以至于仅凭这个原因就足以使用惰性语言。


2

假设您提供了许多昂贵的计算,但是不知道实际需要哪些计算,或者不知道按什么顺序进行计算。您可以添加一个复杂的mother-may-i协议,以迫使消费者确定可用的内容并触发尚未完成的计算。或者,您可以仅提供一个界面,其作用就像所有计算都已完成一样。

另外,假设您有无限的结果。例如所有素数的集合。显然,您无法提前计算集合,因此素数域中的任何操作都必须是惰性的。


1

使用惰性评估,您不会失去对代码执行的控制,它仍然是绝对确定性的。但是,很难使用它。

惰性评估很有用,因为它是减少lambda项的一种方法,这种方法在某些情况下会终止,因为在这种情况下,急切的评估会失败,反之亦然。这包括1)当您需要在实际执行计算之前链接到计算结果时,例如,当您构造循环图结构,但想要以函数样式进行操作时; 2)当您定义无限数据结构,但对该结构起作用时仅使用部分数据结构。

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.