惰性求值如何实现更大程度的模块化?
How does lazy-evaluation allow for greater modularization?
在他的文章“Why Functional Programming Matters”中,John Hughes 认为 "Lazy evaluation is perhaps the most powerful tool for modularization in the functional programmer's repertoire." 为此,他提供了一个这样的例子:
假设您有两个函数,"infiniteLoop" 和 "terminationCondition." 您可以执行以下操作:
terminationCondition(infiniteLoop input)
惰性求值,用 Hughes 的话说 "allows termination conditions to be separated from loop bodies." 这绝对是真的,因为 "terminationCondition" 这里使用惰性求值意味着可以在循环外定义这个条件——当终止条件停止时,无限循环将停止执行求数据。
但是高阶函数不能实现同样的事情吗?
infiniteLoop(input, terminationCondition)
惰性求值如何提供高阶函数无法提供的模块化?
是的,您可以使用传入的终止检查,但要使其正常工作,infiniteLoop
的作者将不得不预见到使用这种条件终止循环的可能性,并硬连线一个在函数中调用终止条件。
而且即使特定的条件可以作为函数传入,它的"shape"也是infiniteLoop
的作者预先设定好的。如果他们给我一个在每个元素上调用的终止条件 "slot" 怎么办,但我需要访问最后几个元素来检查某种收敛条件?也许对于一个简单的序列生成器,您可以想出 "the most general possible" 终止条件类型,但如何做到这一点并保持高效和易于使用并不明显。我是否重复将整个序列传递到终止条件,以防它正在检查?我是否强制我的调用者将他们的简单终止条件包装在一个更复杂的包中,以便它们适合最一般的条件类型?
调用者当然必须确切地知道如何调用终止条件才能提供正确的条件。这可能在很大程度上依赖于这个特定的实现。如果他们切换到另一个第三方编写的 infiniteLoop
的不同实现,那么使用完全相同的终止条件设计的可能性有多大?使用惰性 infiniteLoop
,我可以放弃任何应该产生相同序列的实现。
如果 infiniteLoop
不是 一个简单的序列生成器,而是实际上生成一个更复杂的无限数据结构,如树?如果树的所有分支都是独立递归生成的(想想像国际象棋这样的游戏的移动树),根据迄今为止生成的信息的各种条件,在不同的深度切割不同的分支是有意义的。
如果原作者没有准备好(无论是专门针对我的用例还是足够通用的 class 用例),我真倒霉。 lazy infiniteLoop
的作者可以顺其自然的写,让每个调用者自己懒惰的去探索自己想要的东西;两者都不需要对对方了解太多。
此外,如果停止懒惰地探索无限输出的决定实际上与调用者对该输出所做的计算交错(并依赖于)怎么办?再想想国际象棋的走棋树;我想探索树的一个分支多远很容易取决于我对我在树的其他分支中找到的最佳选择的评估。因此,要么我进行两次遍历和计算(一次在终止条件下 return 一个标志告诉 infinteLoop
停止,然后再次使用有限输出以便我实际上可以得到我的结果),或者infiniteLoop
的作者不仅要为终止条件做准备,还要为一个复杂的函数做准备,这个函数也会得到 return 输出(这样我就可以将我的整个计算推入 "termination condition" 中)。
走极端,我可以探索输出并计算一些结果,将它们显示给用户并获得输入,然后继续探索数据结构(无需根据用户的输入回忆 infiniteLoop
)。懒惰的原作者 infiniteLoop
想不到我会想到做这样的事情,而且它仍然有效。如果我们已经通过类型系统强制执行纯度,那么使用传入的终止条件方法是不可能的,除非在终止条件需要时允许整个 infiniteLoop
产生副作用(比如通过给出整个事情都是一个单子界面)。
简而言之,通过使用采用高阶函数来控制的严格 infiniteLoop
来获得与惰性求值相同的灵活性,对于infiniteLoop
及其调用者(除非公开了各种更简单的包装器,并且其中之一与调用者的用例相匹配)。懒惰评估可以让生产者和消费者几乎完全解耦,同时仍然让消费者能够控制生产者产生多少输出。正如您所说,您可以可以使用额外的函数参数来做任何您可以做的事情,但是它需要生产者和消费者就控制函数如何工作的协议达成实质性一致;并且该协议几乎总是专门针对手头的用例(将消费者和生产者联系在一起)或者为了完全通用而如此复杂以至于生产者和消费者都依赖于该协议,不太可能重新创建其他地方,所以他们仍然绑在一起。
在他的文章“Why Functional Programming Matters”中,John Hughes 认为 "Lazy evaluation is perhaps the most powerful tool for modularization in the functional programmer's repertoire." 为此,他提供了一个这样的例子:
假设您有两个函数,"infiniteLoop" 和 "terminationCondition." 您可以执行以下操作:
terminationCondition(infiniteLoop input)
惰性求值,用 Hughes 的话说 "allows termination conditions to be separated from loop bodies." 这绝对是真的,因为 "terminationCondition" 这里使用惰性求值意味着可以在循环外定义这个条件——当终止条件停止时,无限循环将停止执行求数据。
但是高阶函数不能实现同样的事情吗?
infiniteLoop(input, terminationCondition)
惰性求值如何提供高阶函数无法提供的模块化?
是的,您可以使用传入的终止检查,但要使其正常工作,infiniteLoop
的作者将不得不预见到使用这种条件终止循环的可能性,并硬连线一个在函数中调用终止条件。
而且即使特定的条件可以作为函数传入,它的"shape"也是infiniteLoop
的作者预先设定好的。如果他们给我一个在每个元素上调用的终止条件 "slot" 怎么办,但我需要访问最后几个元素来检查某种收敛条件?也许对于一个简单的序列生成器,您可以想出 "the most general possible" 终止条件类型,但如何做到这一点并保持高效和易于使用并不明显。我是否重复将整个序列传递到终止条件,以防它正在检查?我是否强制我的调用者将他们的简单终止条件包装在一个更复杂的包中,以便它们适合最一般的条件类型?
调用者当然必须确切地知道如何调用终止条件才能提供正确的条件。这可能在很大程度上依赖于这个特定的实现。如果他们切换到另一个第三方编写的 infiniteLoop
的不同实现,那么使用完全相同的终止条件设计的可能性有多大?使用惰性 infiniteLoop
,我可以放弃任何应该产生相同序列的实现。
如果 infiniteLoop
不是 一个简单的序列生成器,而是实际上生成一个更复杂的无限数据结构,如树?如果树的所有分支都是独立递归生成的(想想像国际象棋这样的游戏的移动树),根据迄今为止生成的信息的各种条件,在不同的深度切割不同的分支是有意义的。
如果原作者没有准备好(无论是专门针对我的用例还是足够通用的 class 用例),我真倒霉。 lazy infiniteLoop
的作者可以顺其自然的写,让每个调用者自己懒惰的去探索自己想要的东西;两者都不需要对对方了解太多。
此外,如果停止懒惰地探索无限输出的决定实际上与调用者对该输出所做的计算交错(并依赖于)怎么办?再想想国际象棋的走棋树;我想探索树的一个分支多远很容易取决于我对我在树的其他分支中找到的最佳选择的评估。因此,要么我进行两次遍历和计算(一次在终止条件下 return 一个标志告诉 infinteLoop
停止,然后再次使用有限输出以便我实际上可以得到我的结果),或者infiniteLoop
的作者不仅要为终止条件做准备,还要为一个复杂的函数做准备,这个函数也会得到 return 输出(这样我就可以将我的整个计算推入 "termination condition" 中)。
走极端,我可以探索输出并计算一些结果,将它们显示给用户并获得输入,然后继续探索数据结构(无需根据用户的输入回忆 infiniteLoop
)。懒惰的原作者 infiniteLoop
想不到我会想到做这样的事情,而且它仍然有效。如果我们已经通过类型系统强制执行纯度,那么使用传入的终止条件方法是不可能的,除非在终止条件需要时允许整个 infiniteLoop
产生副作用(比如通过给出整个事情都是一个单子界面)。
简而言之,通过使用采用高阶函数来控制的严格 infiniteLoop
来获得与惰性求值相同的灵活性,对于infiniteLoop
及其调用者(除非公开了各种更简单的包装器,并且其中之一与调用者的用例相匹配)。懒惰评估可以让生产者和消费者几乎完全解耦,同时仍然让消费者能够控制生产者产生多少输出。正如您所说,您可以可以使用额外的函数参数来做任何您可以做的事情,但是它需要生产者和消费者就控制函数如何工作的协议达成实质性一致;并且该协议几乎总是专门针对手头的用例(将消费者和生产者联系在一起)或者为了完全通用而如此复杂以至于生产者和消费者都依赖于该协议,不太可能重新创建其他地方,所以他们仍然绑在一起。