没事瞎思考

没事瞎思考

Clojure Lazy Sequence Snooping

Lazy 的好处是你不会撑死

我是说真的,Lazy 不会吃爆内存, 用多少取多少,按需提供,这才是正确的生产方式。下面我们来见识一下 Lazy 的优势。

先说明一下,本文所有的 Lazy 是指 Lazy Sequence

1 定义无穷序列

1.1 无穷 Fibnoacci 序列

下面我们来定义一个无穷的 Fibonacci 数列,随便你取多少个,只要你的内存允许。下述定义采取了这个规则:第 N 项是 N-1N-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 继续继续判断, 直到找到为止。最后,我不提供证明,其实我不会。

利用排除法解这个问题:

  1. 首先,列出从 2 开始的所有自然数 (2 是第一个素数)
  2. 然后剔除掉所有能被 2 整除的数, 那么列表中下一个数就是 3, 也就是第 2 个素数。
  3. 然后剔除掉所有能被 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 的可变变量共享,这当然是可以,但是这样因小失大了。我提到过,这是陷阱,不是缺陷,如果我们这么做,那么我们就不能重用规则了(机器)。我们让机器允许自定义参数,每个使用的人自己调整一下参数就可以工作,不需要每个人都造一台机器是不是?

Comments

comments powered by Disqus