November 21, 2019
By: Kevin

理解Clojure的LazySeq

  1. 懒到不会动
  2. 勤快到止不住
  3. 杀死本tab的彩蛋

"懒"是clojure的重要特性, 简洁表达意图的同时可以避免付出性能代价.

而且我们可以借助它实现无限序列, 做到按需求值.

;;  打开下面的注释会卡死当前页面
;;  (range)

;; 只取无限序列的10个元素.
(take 10 (range))

好吧,骗你的,其实不会卡死. repl中eval代码的时候做限制了. 在lumo的repl环境中(本blog), 只会输出头1000个, 如果你拿到emacs的cider去试的话, 会发现cider的缓冲区不能大于1M,(range) 会执行输出到14万的时候求值停止。

懒序列在repl会被求值, 因为print的环节需要给用户输出. repl的设计过程中又不希望因为用户偶然输入(比如range忘了传参数)导致的自己卡死, 一旦发现某个语句快的返回值是lazy的时候,repl会比较节制的求值.

懒到不会动

但是在repl环境下, 会造成懒序列会执行的错觉. 而实际的代码执行的时候没有repl去强迫求值. 比如下面的情况:

程序会判断map不需要执行, 因为do表达式会抛弃它的执行结果, 也就是说没人对这个lazyseq求值.

(do (map prn [0 1 2 3 4 5])
    (prn "我前面应该有一堆输出!"))

勤快到止不住

使用懒序列来进行纯粹计算的时候, 结果是符合预期的, 但是依赖它来执行一些副作用的时候, 就要小心了.

(prn "我下面应该有5条输出....对吧?")
(take 5 (map prn (range 10)))

懒序列执行的时候,是按32这个批次来执行的(一个性能优化).

(take 5 (map prn (range 100)))

避免使用懒序列来做副作用操作!!!!

咱们的代码规范中有相应条目:避免依赖lazyseq的副作用

正确的做法:

  1. 避免使用map, reduce, reduce-kv, filter, take-while...等等返回lazyseq的高阶函数都不应该有什么副作用.
  2. 需要副作用的时候考虑
    • doseq
    • dorun
    • transducer, 这个值得再开主题讨论

杀死本tab的彩蛋

;; (prn (range))
(prn "这个真的会卡死, 它绕开了repl对返回值的类型的检测, 所以不会刹车")
Tags: clojure clojurescript