懒惰评估如何允许更大的模块化?

nic*_*n42 8 haskell functional-programming lazy-evaluation higher-order-functions miranda

John Hughes 在他的文章" 为什么功能编程很重要 "中辩称,"懒惰评估可能是函数式程序员模块中最强大的模块化工具." 为此,他提供了一个这样的例子:

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

terminationCondition(infiniteLoop input)
Run Code Online (Sandbox Code Playgroud)

懒惰的评价,在休斯的话中"允许终止条件与循环体分离".这绝对是正确的,因为这里使用延迟评估的"terminationCondition"意味着可以在循环外定义这个条件 - 当terminationCondition停止请求数据时,infiniteLoop将停止执行.

但高阶函数不能达到如下相同的效果吗?

infiniteLoop(input, terminationCondition)
Run Code Online (Sandbox Code Playgroud)

懒惰评估如何提供高阶函数不提供的模块化?

Ben*_*Ben 13

是的,你可以使用传入的终止检查,但为了工作,作者infiniteLoop必须预见到想要用这种条件终止循环的可能性,并硬连线调用终止条件到他们的函数中.

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

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

如果infiniteLoop 不是一个简单的序列生成器,但实际上生成一个更复杂的无限数据结构,如树?如果树的所有分支都是独立递归生成的(考虑像国际象棋这样的游戏的移动树),基于到目前为止生成的信息的各种条件,切割不同深度的不同分支是有意义的.

如果原作者没有准备(专门针对我的用例或足够一般的用例类别),我运气不好.懒惰的作者infiniteLoop可以用自然的方式写出来,让每个来电者懒洋洋地探索他们想要的东西; 他们根本不需要了解对方.

如果停止懒惰地探索无限输出的决定实际上与调用者正在对该输出进行的计算交错(并且依赖于),该怎么办?再想想国际象棋移动树; 我想要探索树的一个分支可以轻松地取决于我对树的其他分支中找到的最佳选项的评估.所以要么我做遍历和计算两次(一次在终止条件下返回一个标志告诉infinteLoop停止,然后再次使用有限输出,所以我实际上可以得到我的结果),或者作者infiniteLoop必须准备不仅仅一个终止条件,但是一个复杂的函数也可以返回输出(这样我就可以将我的整个计算推送到"终止条件").

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

简而言之,为了获得与延迟评估相同的灵活性,通过使用严格infiniteLoop来获取更高阶函数来控制它可能会给作者infiniteLoop及其调用者带来大量额外的复杂性(除非各种更简单的包装器)暴露,其中一个匹配调用者的用例).懒惰评估可以使生产者和消费者几乎完全脱钩,同时仍然让消费者能够控制生产者产生多少输出.如你所说,你可以用这种方式做所有你可以用额外的函数参数做的事情,但它要求生产者和消费者基本上就控制功能如何工作的协议达成一致; 并且该协议几乎总是专门针对手头的用例(将消费者和生产者联系在一起)或者如此复杂,以便完全一般地将生产者和消费者绑定到该协议,这不太可能被重建其他地方,所以他们仍然绑在一起.