某些语言(Haskell、Clojure、Scheme 等)具有惰性求值。惰性求值的“卖点”之一是无限数据结构。这有什么了不起的?能够处理无限数据结构显然具有优势的案例有哪些?
6 回答
举两个例子,一大一小:
约翰休斯的《为什么函数式编程很重要》有一个很好的例子,一个国际象棋游戏。国际象棋游戏的移动树实际上并不是无限的,但它足够大以至于它也可能是无限的(称之为“接近无限”)。在严格的语言中,您实际上不能将其视为一棵树,因为没有足够的空间来存储整棵树。但是在惰性语言中,您只需定义树,然后定义“nextMove”函数以尽可能地遍历它。惰性评估机制负责处理细节。
这个小例子只是简单地将索引号与列表中的每个项目相关联,因此 ["foo", "bar", "baz"] 变为 [(1,"foo"), (2,"bar"), ( 3,“巴兹”)]。在严格的语言中,您需要一个循环来跟踪最后一个索引并检查您是否在最后。在 Haskell 中,您只需说:
zip [1..] items
zip 的第一个参数是一个无限列表。您无需提前计算出需要多长时间。
我能想到的几个优点:
- 更简洁的代码——有趣的是,生成无限序列的代码通常比对有界数据进行操作的代码更简单、更简洁。这是因为这样的代码通常更接近底层的数学定义。
- 灵活性——与其提前决定你的数据需要多大,如果你使用惰性无限结构来编写它,你根本不需要担心。它会“正常工作”
- 性能——如果你正确地使用惰性,你可以通过只在需要时计算数据值来使用它来提高性能——而且可能根本不需要。因此,您可以潜在地避免大量不必要的计算。
- 无限进程的抽象——无限惰性序列作为事件流的抽象有一个有趣的用途,例如随着时间的推移从网络连接中读取输入。这可以创建一些非常优雅和有趣的方式来创建代码来处理事件流。例如,请参阅 Clojure 中的stream-utils库。
- 更好的算法- 有些算法更容易用无限数据结构表达 - 想法是你懒惰地“拉入”你需要的解决方案部分,同时不评估无限算法的其余部分。如果使用这种方法启用您可以降低算法的时间复杂度(例如 from
O(n^2)
toO(n log n)
),那么这可能是一个巨大的胜利。
有规范的纯记忆策略:
fib = (map fib' [0..] !!)
where
fib' 0 = 0
fib' 1 = 1
fib' n = fib (n-1) + fib (n-2)
我们将fib'
函数映射到一个无限列表上,以构建一个包含 的所有值的表fib
。瞧!便宜,容易记忆。
当然,这在参数中具有线性查找时间。您可以将其替换为无限特里以获取对数查找时间。参看。数据完整性。
我打算评论@knivil 的计划。相反,我将把它作为另一个答案提出来。
惰性数据结构并不是完成大多数任务的唯一方法。这可能会激怒 Python 爱好者。但我相信最好让程序员选择他们使用的技术。懒惰的技术是强大而优雅的。
Knivil 提到使用 Scheme 的iota
. 看看编写完整的方法(使用所有 3 个参数)是多么容易,依赖于惰性:
iota count begin step = let xs = begin:map (+step) xs
in take count xs
-- or, alternately
iota count begin step = take count $ map ((+begin).(*step)) [0..]
我还可以length
通过滥用懒惰来写非空列表:
len = fst . last . zip [1..]
-- or even handling empty lists
len = fst . last . zip [0..] . (undefined:)
考虑一下 Prelude 中定义的强大而优雅的iterate
函数。
iterate f x = x : iterate f (f x)
它创建了无限列表[x, f x, f (f x), f (f (f x)), ...]
。我本可以这样写iota
:iterate
iota count begin step = take count $ iterate (+step) begin
惰性方法是一种优雅的编程方式。这不是唯一的方法,习惯了 C 或 Java 的人肯定会喊“但我不需要懒惰,我可以只是 _”,他们是正确的。如果您的语言是图灵完备的,则可以完成。但是懒惰可以如此优雅。
好吧,上个月我有一个很好的用例。复制对象时,我需要一个生成器来生成唯一名称。这意味着,生成器采用原始名称X
,并为副本生成一个新名称。它通过附加一个文本来做到这一点
X - copy
X - copy (2)
X - copy (3)
...
只要该名称未在同一组内的一组对象中使用。为此使用“无限数据结构”(无限的字符串数组)而不是简单的循环有一个优点:如果名称已经在使用中,您可以将名称生成部分与测试完全分开。所以我可以为不同类型的对象重用生成器函数,其中每个对象类型的使用测试略有不同。
无限数据结构提供了(可计算的)实数的优雅表示。例如,一个无限列表,如
[(0, 4), (1, 3.5), (3.1, 3.2), ...]
可以代表pi
。借助内置的惰性,对这种表示进行操作变得容易。