表达式的惰性求值似乎会使程序员失去对代码执行顺序的控制。我很难理解为什么程序员会接受或期望这样做。
当我们无法保证将在何时何地评估表达式时,该范式如何用于构建可预期运行的可预测软件?
表达式的惰性求值似乎会使程序员失去对代码执行顺序的控制。我很难理解为什么程序员会接受或期望这样做。
当我们无法保证将在何时何地评估表达式时,该范式如何用于构建可预期运行的可预测软件?
Answers:
许多答案都涉及到无限列表和未计算部分的性能提升等问题,但这却缺少了懒惰的更大动机:模块化。
约翰·休斯(John Hughes )广为引用的论文“为什么要进行功能编程”(PDF链接)中阐述了这一经典论点。该论文(第5节)中的关键示例是使用alpha-beta搜索算法播放井字游戏。关键是(第9页):
[惰性评估]使得将程序模块化为实用工具,使其能够生成大量可能的答案以及选择适当答案的选择器。
Tic-Tac-Toe程序可以编写为从给定位置开始生成整个游戏树的函数,也可以编写为消耗该函数的单独函数。在运行时,这不会本质上生成整个游戏树,而只会生成消费者实际需要的那些子部分。我们可以通过改变消费者来改变生产替代品的顺序和组合。根本不需要更换发电机。
用一种急切的语言,您不能以这种方式编写它,因为您可能会花费太多时间和内存来生成树。因此,您最终要么:
当我们无法保证将在何时何地评估表达式时,该范式如何用于构建可预期运行的可预测软件?
当表达式没有副作用时,表达式的计算顺序不会影响其值,因此程序的行为不受该顺序的影响。因此,行为是完全可以预测的。
现在副作用是另一回事了。如果副作用可能以任何顺序发生,则该程序的行为确实是不可预测的。但这实际上并非如此。像Haskell这样的惰性语言很重要,它具有参照透明性,即确保表达式的求值顺序不会影响其结果。在Haskell中,这是通过强制所有具有用户可见副作用的操作在IO monad内部进行来实现的。这样可以确保所有副作用均按您期望的顺序发生。
如果您熟悉数据库,则处理数据的一种非常常见的方法是:
select * from foobar
您无法控制结果的生成方式以及生成索引的方式(索引,全表扫描吗?)或生成时间(何时要求生成所有数据?您所知道的是:如果有更多数据,您将在需要时得到它。
惰性评估非常接近同一件事。假设您有一个定义为ie的无限列表。斐波那契数列-如果您需要五个数字,则可以计算五个数字;如果需要1000,则会得到1000。诀窍是运行时知道何时何地提供什么。非常方便。
(Java程序员可以使用Iterators模仿这种行为-其他语言可能具有类似的功能)
Collection2.filter()
(以及该类中的其他方法)几乎实现了惰性求值:结果“看起来”很普通Collection
,但是执行的顺序可能是不直观的(或至少不是显而易见的)。另外,yield
Python中有一个(C#中有一个类似的功能,我不记得它的名字),它比普通的Iterator更接近于支持懒惰求值。
考虑向数据库询问名称以“ 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”的问题。在类似Haskell的东西中,您可以编写
take 10 . filter (<1) . map (1/)
但这在严格的语言中是不正确的,因为如果您给出它[2,3,4,5,6,7,8,9,10,11,12,0]
,则将被零除。请参阅sacundim的答案,以了解为什么这在实践中很棒
更多事情起作用严格(双关语意)更多程序终止于非严格评估而不是严格评估。如果您的程序以“急切”的评估策略终止,则它将以“懒惰”的评估策略终止,但是相反的说法是不正确的。您会得到诸如无限数据结构(实际上只是很酷)之类的东西作为这种现象的具体示例。更多程序可以使用惰性语言。
最优性按需 调用评估相对于时间是渐近最优的。尽管主要的惰性语言(本质上是Haskell和Haskell)不保证按需致电,但您可以或多或少地期望最佳成本模型。严格的分析器(和投机评估)在实践中降低了开销。空间是一个更复杂的问题。
使用惰性评估来实现“ 强制纯净”会使您无序地应对副作用,因为如您所说,程序员会失去控制。这是一件好事。参照透明性使对程序的编程,验证和推理变得非常容易。严格的语言不可避免地会屈服于具有不纯净的位的压力-Haskell和Clean曾对此进行了漂亮的抵抗。这并不是说副作用总是有害的,但是控制副作用是如此有用,以至于仅凭这个原因就足以使用惰性语言。