Clojure Lazy Sequence Snooping
Table of Contents
Lazy 的好处是你不会撑死
我是说真的,Lazy 不会吃爆内存, 用多少取多少,按需提供,这才是正确的生产方式。下面我们来见识一下 Lazy 的优势。
先说明一下,本文所有的 Lazy 是指 Lazy Sequence
1 定义无穷序列
1.1 无穷 Fibnoacci 序列
下面我们来定义一个无穷的 Fibonacci 数列,随便你取多少个,只要你的内存允许。下述定义采取了这个规则:第 N
项是 N-1
和 N-2
项的和。知道第 1,2 项 [0 1]
就知道所有项了。
(def fib-seq (lazy-cat [0 1] (map + fib-seq (rest fib-seq))))
1.2 无穷素数序列
换个规则,我们采取同样的方式定义素数,素数有这么一个规则: 假设我们找到了前 N
个素数,那么第 N+1
个素数的寻找方法是(假设第 N
个素数是 x),设定 x+1
,如果 x+1
不能整除 N
个素数当中的任何一个, x+1
就是第 N+1
个素数,如果能整除,那么设定 x+2
继续继续判断, 直到找到为止。最后,我不提供证明,其实我不会。
利用排除法解这个问题:
- 首先,列出从 2 开始的所有自然数 (2 是第一个素数)
- 然后剔除掉所有能被 2 整除的数, 那么列表中下一个数就是 3, 也就是第 2 个素数。
- 然后剔除掉所有能被 3 整除的数,那么列表中下一个数就是 5, 也就是第 3 个素数。
- …
(def prime-seq ((fn filter-prime [[x & xs]] (cons x (lazy-seq (filter-prime (remove #(zero? (mod % x)) xs))))) (drop 2 (range))))
Lazy 的好处就是,如果没有 Lazy, 上诉第 1 步 所有自然数
都是不可能的。有了 Lazy,很多在我们头脑中的概念可以轻易表达了。
2 动态作用域陷阱
说了这么多 Lazy 的好处,如果你们想用它,我觉得有必要提一下他的陷阱。我们通过上面的例子,可以看出,使用 Lazy 的前提条件你的知道持续生成的规则,这就相当于某个生产机器。如果机器内部的某个零件被更换了,那么你下次生产出来的产品有可能就不合格了。当然,这对于函数式语言来说,这生产机器就是个黑盒子,一旦造好,就不可篡改,这可以减少很多 Bug,但是仍然免不了需要引用某个外部变量,这个时候,正确性就不保证了,我们来看下面这个例子:
(def ^:dynamic *x* 3) (def xs (binding [*x* 7] (map (partial + *x*) (range 10)))) (println xs) ;;=> (3 4 5 6 7 8 9 10 11 12)
上诉程序我们期望得到的结果是 (7 8 9 ...)
, 这是因为你设定 *x*
为 7 的时候,机器并没有生产,正式生产数据时, *x*
又变回 3 了。 有什么办法解决吗?答案是有的,在机器的类比中,我们可以复制一个 *x*
让其变为内部变量,这也就是所谓的闭包。
(def ^:dynamic *x* 3) (def xs (binding [*x* 7] (let [x *x*] (map (partial + x) (range 10))))) (println xs) ;;=> (7 8 9 10 11 12 13 14 15 16)
现在是我们想要的结果了。我们既然这么采取复制的方式来保证纯洁性,那么我们是否可以禁止 Clojure 的可变变量共享,这当然是可以,但是这样因小失大了。我提到过,这是陷阱,不是缺陷,如果我们这么做,那么我们就不能重用规则了(机器)。我们让机器允许自定义参数,每个使用的人自己调整一下参数就可以工作,不需要每个人都造一台机器是不是?