约翰·休斯(John Hughes)在他的文章“Why Functional Programming Matters”中指出,“懒惰的评估也许是功能性程序员的全部模块化中最强大的工具。”为此,他提供了以下示例:

假设您有两个函数,“infiniteLoop”和“terminationCondition”。您可以执行以下操作:

terminationCondition(infiniteLoop input)

用休斯的话说的懒惰的评价是“允许将终止条件与环体分开”。这是绝对正确的,因为此处使用惰性求值的“terminationCondition”意味着可以在循环外部定义此条件-当terminationCondition停止请求数据时,infiniteLoop将停止执行。

但是高阶函数不能实现以下相同功能吗?
infiniteLoop(input, terminationCondition)

惰性求值如何提供模块化功能,而高阶函数无法提供模块化功能?

最佳答案

是的,您可以使用传递的终止检查,但是要做到这一点,infiniteLoop的作者必须预料要使用这种条件终止循环的可能性,并将对终止条件的调用硬连接到其函数中。

并且即使可以将特定条件作为函数传递,它的“形状”也由infiniteLoop的作者预先确定。如果它们给我一个在每个元素上调用的终止条件“slot”,但我需要访问最后几个元素以检查某种收敛条件怎么办?也许对于一个简单的序列生成器,您可以想出“最通用的”终止条件类型,但是如何做到并保持效率和易用性却并不明显。如果要检查的是终止序列,我是否会重复将整个序列传递到终止条件?我是否要迫使调用者将其简单的终止条件包装在一个更复杂的程序包中,以便它们适合最一般的条件类型?

调用者当然必须确切地知道如何终止条件才能提供正确的条件。这可能相当依赖于此特定的实现。如果他们切换到另一个第三方编写的infiniteLoop的不同实现,那么对于终止条件使用完全相同的设计的可能性有多大?使用懒惰的infiniteLoop,我可以放入应该产生相同序列的任何实现中。

如果infiniteLoop不是简单的序列生成器,而是实际上生成了更复杂的无限数据结构(如树),该怎么办?如果树的所有分支都是独立递归生成的(例如象棋这样的游戏的移动树),则根据到目前为止所生成信息的各种条件,在不同深度切开不同的分支是有意义的。

如果原始作者没有准备(专门针对我的用例或足够普通的用例类别),那么我很不走运。懒惰的infiniteLoop的作者可以自然地编写它,并让每个调用者懒洋洋地探索他们想要的东西。彼此之间都不必了解太多。

此外,如果停止懒惰探索无限输出的决定实际上与调用者对该输出进行的计算交错在一起(并取决于该调用者进行的计算),该怎么办?再想一想象棋移动树;我想要探索树的一个分支的距离可以轻松地取决于我对在树的其他分支中找到的最佳选择的评估。因此,要么我进行两次遍历和计算(在终止条件下返回一个指示infinteLoop停止的标志,然后再次使用有限的输出,这样我才能真正得到结果),或者infiniteLoop的作者必须做准备不仅是终止条件,而且还有一个复杂的函数,该函数还可以返回输出(这样我就可以将整个计算插入“终止条件”内)。

极端地讲,我可以探索输出并计算一些结果,将其显示给用户并获得输入,然后继续探索数据结构(无需根据用户的输入来调用infiniteLoop)。懒惰的infiniteLoop的原始作者根本不知道我会想到这样做,并且仍然可以使用。如果我们已经通过类型系统强制执行了纯度,那么传入的终止条件方法将不可能实现这一点,除非终止条件需要使整个infiniteLoop产生副作用(例如,通过给整个对象加一个单子(monad))界面)。

简而言之,要想获得与懒惰评估相同的灵活性,使用严格的infiniteLoop并采用高阶函数来控制它,对于infiniteLoop的作者及其调用者来说,可能会额外增加很多复杂性(除非更简单的包装器是公开的,其中之一与调用者的用例匹配。惰性评估可以使生产者和消费者几乎完全脱钩,同时仍然使消费者能够控制生产者产生多少产出。正如您所说的那样,您可以执行的所有操作都可以使用额外的函数参数来完成,但是这要求生产者和使用者必须就控制功能如何工作达成协议(protocol)。并且该协议(protocol)几乎总是要么专用于手边的用例(将消费者和生产者联系在一起),要么过于复杂以至于无法将其重新创建,从而使生产者和消费者都依赖于该协议(protocol)。在其他地方,因此它们仍然捆绑在一起。

10-07 22:02