8

在他的文章“为什么函数式编程很重要”中,John Hughes 认为“惰性求值可能是函数式程序员所有的模块化最强大的工具”。为此,他提供了一个这样的示例:

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

terminationCondition(infiniteLoop input)

用 Hughes 的话来说,惰性求值“允许将终止条件与循环体分开”。这绝对是正确的,因为这里使用惰性求值的“terminationCondition”意味着可以在循环之外定义这个条件——当 terminateCondition 停止请求数据时,infiniteLoop 将停止执行。

但是高阶函数不能实现如下相同的事情吗?

infiniteLoop(input, terminationCondition)

惰性求值如何在这里提供高阶函数不提供的模块化?

4

1 回答 1

14

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

而且即使具体的条件可以作为函数传入,它的“形状”也是由作者预先确定的infiniteLoop。如果他们给我一个在每个元素上调用的终止条件“槽”,但我需要访问最后几个元素来检查某种收敛条件怎么办?也许对于一个简单的序列生成器,您可以提出“最通用的”终止条件类型,但是如何做到这一点并保持高效和易于使用并不明显。到目前为止,我是否将整个序列反复传递到终止条件中,以防它正在检查?我是否强制我的调用者将他们简单的终止条件包装在一个更复杂的包中,以便它们适合最一般的条件类型?

为了提供正确的条件,调用者当然必须确切地知道终止条件是如何被调用的。这可能在很大程度上依赖于这个特定的实现。infiniteLoop如果他们切换到由另一个第三方编写的不同实现,那么使用完全相同的终止条件设计的可能性有多大?使用 lazy infiniteLoop,我可以插入任何应该产生相同序列的实现。

如果infiniteLoop 不是一个简单的序列生成器,而是实际上生成一个更复杂的无限数据结构,比如一棵树呢?如果树的所有分支都是独立递归生成的(想想象棋之类的游戏的移动树),那么根据迄今为止生成的信息的各种条件,在不同深度切割不同的分支是有意义的。

如果原作者没有准备好(专门针对我的用例或足够通用的用例类别),我就不走运了。懒的作者infiniteLoop可以就这么自然的写出来,让每个单独的调用者懒洋洋的探索自己想要什么;双方都不需要对对方了解太多。

此外,如果停止懒惰地探索无限输出的决定实际上与调用者对该输出进行的计算交错(并依赖于)呢?再想想国际象棋走法树;我想探索树的一个分支多远很容易取决于我对在树的其他分支中找到的最佳选项的评估。因此,要么我进行两次遍历和计算(一次在终止条件下返回一个指示infinteLoop停止的标志,然后再次使用有限输出,这样我才能真正得到我的结果),或者作者infiniteLoop不仅要为一个终止条件,但是一个复杂的函数也可以返回输出(这样我就可以将我的整个计算推到“终止条件”中)。

走极端,我可以探索输出并计算一些结果,将它们显示给用户并获取输入,然后继续探索数据结构(无需infiniteLoop根据用户的输入进行召回)。懒惰的原作者infiniteLoop根本不知道我会想到做这样的事情,而且它仍然可以工作。如果我们已经通过类型系统强制执行纯度,那么使用传入的终止条件方法是不可能的,除非在infiniteLoop终止条件需要时允许整体产生副作用(例如通过给整个事物一个单子接口)。

简而言之,为了获得与惰性求值相同的灵活性,通过使用需要更高阶函数来控制它的严格函数,对于作者及其调用者infiniteLoop来说可能会产生大量额外的复杂性(除非各种更简单的包装器infiniteLoop被暴露,其中之一与调用者的用例相匹配)。惰性评估可以让生产者和消费者几乎完全解耦,同时仍然让消费者能够控制生产者产生多少输出。你能做到的一切都是你能做到如您所说,使用额外的函数参数,但它要求生产者和消费者基本上就控制功能如何工作的协议达成一致;并且该协议几乎总是专门针对手头的用例(将消费者和生产者捆绑在一起)或非常复杂以完全通用以致生产者和消费者都与该协议相关联,而该协议不太可能被重新创建其他地方,所以他们仍然绑在一起。

于 2016-12-20T01:15:55.093 回答