John Hughes在他的文章“Why Functional Programming Matters”中辩称,“懒惰的评估可能是功能程序员所有模块化中最强大的工具。”为此,他提供了一个这样的例子:
假设您有两个函数,“infiniteLoop”和“terminationCondition”。您可以执行以下操作:
terminationCondition(infiniteLoop input)
懒惰的评价,在休斯的话中“允许终止条件与循环体分离”。这绝对是正确的,因为这里使用延迟评估的“terminationCondition”意味着可以在循环外定义这个条件 - 当terminationCondition停止请求数据时,infiniteLoop将停止执行。
但是高阶函数不能达到如下相同的效果吗?
infiniteLoop(input, terminationCondition)
延迟评估如何在此处提供高阶函数不提供的模块化?
答案 0 :(得分:13)
是的,你可以使用传入的终止检查,但为了工作,infiniteLoop
的作者将不得不预见到想要在这种情况下终止循环的可能性,并硬连线调用终止条件进入他们的职能。
即使特定的条件可以作为函数传递,它的“形状”也由infiniteLoop
的作者预先确定。如果他们给我一个在每个元素上调用的终止条件“slot”怎么办,但我需要访问最后几个元素来检查某种收敛条件?也许对于一个简单的序列生成器,您可以提出“最常见的”终止条件类型,但是如何这样做并且保持高效且易于使用并不明显。到目前为止,我是否重复将整个序列传递到终止条件,以防它正在检查什么?我是否强制我的调用者将其简单的终止条件包装在一个更复杂的包中,以便它们适合最常见的条件类型?
调用者必须确切地知道如何调用终止条件以提供正确的条件。这可能是对这个具体实现的相当依赖。如果他们切换到另一个第三方编写的infiniteLoop
的不同实现,那么使用终止条件的完全相同设计的可能性有多大?使用惰性infiniteLoop
,我可以放入任何应该产生相同序列的实现。
如果infiniteLoop
不是一个简单的序列生成器,但实际上生成一个更复杂的无限数据结构,如树?如果树的所有分支都是独立递归生成的(考虑像国际象棋这样的游戏的移动树),根据迄今为止生成的信息的各种条件,在不同深度切割不同的分支是有意义的。
如果原作者没有准备(专门针对我的用例或足够一般的用例类别),我运气不好。懒惰infiniteLoop
的作者可以用自然的方式写出来,让每个来电者懒洋洋地探索他们想要的东西;他们根本不需要了解其他人。
此外,如果停止懒惰地探索无限输出的决定实际上是与调用者对该输出进行的计算交错(并且依赖于)?再想想国际象棋移动树;我想要探索树的一个分支可以轻松地取决于我对树的其他分支中找到的最佳选项的评估。所以要么我进行遍历和计算两次(一次在终止条件下返回一个告诉infinteLoop
停止的标志,然后再次使用有限输出,这样我实际上可以得到我的结果),或{的作者{1}}不仅要准备一个终止条件,还要准备一个复杂的函数来返回输出(这样我就可以将整个计算推到“终止条件”中)。
走极端,我可以探索输出并计算一些结果,将它们显示给用户并获得输入,然后继续探索数据结构(不根据用户的输入调用infiniteLoop
)。懒惰的infiniteLoop
需要的原始作者不知道我会想到做这样的事情,它仍然可以工作。如果我们已经通过类型系统强制执行纯度,那么使用传入的终止条件方法是不可能的,除非在终止条件需要时允许整个infiniteLoop
具有副作用(例如通过给出整个事物是monadic接口)。
简而言之,为了获得与延迟评估相同的灵活性,通过使用严格的infiniteLoop
来获取更高阶函数来控制它,对于{{{{{{{ 1}}及其调用者(除非暴露出各种更简单的包装器,其中一个与调用者的用例相匹配)。懒惰评估可以使生产者和消费者几乎完全脱钩,同时仍然让消费者能够控制生产者产生多少输出。如你所说,你可以用 做的所有事情都可以用额外的函数参数来做,但它要求生产者和消费者基本上就控制功能如何工作的协议达成一致;并且该协议几乎总是专门针对手头的用例(将消费者和生产者联系在一起)或者如此复杂,以便完全一般地将生产者和消费者绑定到该协议,这不太可能被重建其他地方,所以他们仍然联系在一起。