clojure
懒惰可能是一件好事。 也许最好的事情?
当然,在计算的后端,懒惰是强大的。 Clojure在许多方面都很漂亮,而懒惰是其核心组成部分。 关于它以及它周围的大量文章。 它们涵盖了如何编写此类代码的各个方面。 大多数情况下,例如斐波那契(Fibonacci)或通用数字生成器。 随之而来的是一些问题。 我肯定有一些-
- 对于新手,来自其他编程风格的人来说,是否容易理解? 收养的障碍?
- 如何将其与您和我正在研究的实际问题联系起来?
- 采用和将其转移到“生产”的障碍是什么?
- 有哪些陷阱和谬论?
因此,这是回答我懒惰时所汲取的教训的一种方法。
什么是懒惰?
首先是“什么”。 懒惰,即懒惰的求值是代码的一部分,直到绝对需要它们的返回值时才进行求值。 在类似的思路上考虑一下,因为我们推迟了提交纳税申报表的时间,直到最后一次提交。 对于我们来说,去做在此期间实际上有用的事情非常强大。 我们可以尽早提起诉讼,但没有,也没有必要。 这绝对使我感到“有效率”。 (旁注:为获得更好的双关语,请查看以下参考资料中的帖子(:)
将其带回后端,有些表达式直到流程中的后续阶段才需要完全求值。 在那个水平上是有意义的。 现在是“如何”。 在这一部分中,让我们仔细研究Clojure的核心原则。 懒惰的评估绝对是其中之一。 特别地,惰性是通过可以在函数之间传递的惰性序列构造的,并且仅当发出eval命令时才进行评估。 这是我的意思的简要说明(如https://clojuredocs.org/clojure.core/lazy-seq所示 )
;; The following defines a lazy-seq of all positive numbers. Note that
;; the lazy-seq allows us to make a recursive call in a safe way because
;; the call does not happen immediately but instead creates a closure.
user=> (defn positive-numbers
([] (positive-numbers 1))
([n] (lazy-seq (cons n (positive-numbers (inc n))))))
#'user/positive-numbers
user=> (take 5 (positive-numbers))
(1 2 3 4 5)
太棒了! 有了这,这是我的第一堂课
第1课-很棒,我想偷懒,但是我到底想偷懒做什么?
为什么懒惰?
“你为什么问。 总是有部分代码在不需要之前才需要完成。 即使他们这样做了,我们也不需要评估中的所有返回值,也许只是n
个(count allresults)
这听起来似乎具有相当的优势。
让我们打破positive-numbers
例子,不要懒惰,不涉及潜在的成本。
(defn not-lazy-positive-numbers [n]
(mapv
#(let [v (inc %)]
; to know when evaluation happens
(println "executing" v)
v)
(range (- n 1) (+ n 10))))
;returns n to n+10 values starting from n
(not-lazy-positive-numbers 10)
;executing 10
;executing 11
;executing 12
;executing 13
;executing 14
;executing 15
;executing 16
;executing 17
;executing 18
;executing 19
;executing 20
;[10 11 12 13 14 15 16 17 18 19 20]
not-lazy-positive-numbers
立即评估。 为了说明这一点,假设我们必须从10到40之间的正数池中选择15个数字。
(take 15 (concat (not-lazy-positive-numbers 10) (not-lazy-positive-numbers 20) (not-lazy-positive-numbers 30)))
;executing 10
;executing 11
;executing 12
;executing 13
;executing 14
;executing 15
;executing 16
;executing 17
;executing 18
;executing 19
;executing 20
;executing 20
;executing 21
;executing 22
;executing 23
;executing 24
;executing 25
;executing 26
;executing 27
;executing 28
;executing 29
;executing 30
;executing 30
;executing 31
;executing 32
;executing 33
;executing 34
;executing 35
;executing 36
;executing 37
;executing 38
;executing 39
;executing 40
(10 11 12 13 14 15 16 17 18 19 20 20 21 22 23)
哇,即使我们只需要其中15个值,它也会评估所有值,即30+。
第2课-有很多代码节可以完成所需的工作,请查找它们
现在,从原始示例中应用一些懒惰,
(defn lazy-positive-numbers [n]
(println "executing" n) ; to know what's executing, returns a lazy seq of max 1+10 executions
(lazy-seq (cons n (take 10 (lazy-positive-numbers (inc n))))))
(lazy-positive-numbers 10)
;executing 10
;executing 11
;executing 12
;executing 13
;executing 14
;executing 15
;executing 16
;executing 17
;executing 18
;executing 19
;executing 20
;executing 21
;(10 11 12 13 14 15 16 17 18 19 20)
lazy-positive-numbers
总是作为延迟序列返回10个值。 (旁注—懒惰seq的丑陋代码,可能是线程持久的:/)
再说一次,我们必须从10到40之间的正数池中选择15个数字。
(take 15 (concat (lazy-positive-numbers 10) (lazy-positive-numbers 20) (lazy-positive-numbers 30)))
;executing 10
;executing 20
;executing 30
;executing 11
;executing 12
;executing 13
;executing 14
;executing 15
;executing 16
;executing 17
;executing 18
;executing 19
;executing 20
;executing 21
;executing 21
;executing 22
;executing 23
;executing 24
;(10 11 12 13 14 15 16 17 18 19 20 20 21 22 23)
太棒了! 使用惰性评估,执行次数减少到18(对于主要源的初始化为-3)。
第3课-懒惰会带来创建顶级初始化的开销。 更多的惰性源,更多的开销。
关于懒惰IRL
(不重要的课程-IRL =“现实生活中”)
现在让我们举一个真实的例子。 我们正在呈现一个包含帖子和更新的用户供稿页面。 返回的帖子数基于某些相关性参数并进行了分页。 这些帖子来自多个来源,以根据收到的不同信号构建实时提要。 就像一个业务流程。 来源可以来自数据库,ML数据模型,缓存,并回填了一些预设数据。 所有源都有检索数据的计算量和等待时间。 在伪代码中,它看起来应该像
posts = [];
until(posts.length >= limit;
sourcelist = get-data-sources()
data = get-data-from-sources()
remdata = data.slice(posts.length - limit - data.length)
posts.push(remdata))
看起来很懒惰。 Sourcelist中的每个条目都是一个生成器,即返回可以合并在一起,转换并添加到返回的帖子中的一系列数据。 应用惰性原则,所有方法都看起来像
(defn get-data-sources []
(lazy-seq
[#(lazy-positive-numbers 10) ;data generators, can be replaced with actual db calls
#(lazy-positive-numbers 20)
#(lazy-positive-numbers 30)]))
(defn get-data-from-sources [sourcelist]
(map #(apply % []) sourcelist))
(defn get-posts [limit]
(->>
(get-data-sources) ;returns seq of data-sources
(get-data-from-sources) ;returns a lazy-seq of results
(apply concat) ;concat all lazy-seq before taking
(take limit)))
;Executing should call other data sources only after exhausting the current one
(get-posts 15)
;executing 10
;executing 20
;executing 30
;executing 11
;executing 12
;executing 13
;executing 14
;executing 15
;executing 16
;executing 17
;executing 18
;executing 19
;executing 20
;executing 21
;executing 21
;executing 22
;executing 23
;executing 24
;(10 11 12 13 14 15 16 17 18 19 20 20 21 22 23)
oo! 想象一下,执行40次调用以检索15个条目,。
第4课-懒惰有助于填补来自不同来源的序列。
另一个值得注意的重要事实是,每个动作都从伪代码分解为功能。 如果没有功能,那么使用懒惰将变得更加困难。
第5课-如果它不起作用,则很难偷懒。
懒惰有多快?
懒惰通常会使人感到低速。 但是是这样吗? 在输出中添加一个很小的工具
(defn eval-not-lazy []
(time
(let [result (take 15 (concat (not-lazy-positive-numbers 10) (not-lazy-positive-numbers 20) (not-lazy-positive-numbers 30)))]
(println result))))
(defn eval-lazy []
(time
(let [result (take 15 (concat (lazy-positive-numbers 10) (lazy-positive-numbers 20) (lazy-positive-numbers 30)))]
(println result))))
(eval-not-lazy)
;(10 11 12 13 14 15 16 17 18 19 20 20 21 22 23)
;"Elapsed time: 0.571285 msecs"
;nil
;(eval-lazy)
;(10 11 12 13 14 15 16 17 18 19 20 20 21 22 23)
;"Elapsed time: 0.415577 msecs"
;nil
绝对与非延迟版本的速度相同。
第6课-懒惰不会对速度产生负面影响,它可以相同或更好
并发和懒惰
现在,让我们看看惰性对并发执行有何React,甚至是否可以使用并发。 在我们的示例中,使用pmap
并行执行。 (Clojure的喜悦:))。
(defn parallel-get-data-from-sources [sourcelist]
(pmap #(apply % []) sourcelist))
(defn parallel-get-posts [limit]
(->>
(get-data-sources) ;returns seq of data-sources
(parallel-get-data-from-sources) ;returns a lazy-seq of results
(apply concat) ;concat all lazy-seq before taking
(take limit)))
(parallel-get-posts 15)
;executingexecuting 1020
;executing 30
;executing 11
;executing 12
;executing 13
;executing 14
;executing 15
;executing 16
;executing 17
;executing 18
;executing 19
;executing 20
;executing 21
;executing 21
;executing 22
;executing 23
;executing 24
;(10 11 12 13 14 15 16 17 18 19 20 20 21 22 23)
是的,它可以使用,但没有意义,因为我们要填充一个序列,除非可以并行进行初始化。
第7课-并发不影响惰性,序列按顺序填充
Gotcha的
像那里的所有锤子一样,这是一把锤子,可服务于特定的一组钉子,并且最好了解何时不应该使用它。 Clojure的许多核心功能(如take
, map
, repeat
等)返回惰性序列。 在应评估整个表达式集的情况下,应将懒惰性短路以评估所有表达式。
在我们的例子中,短路可以与被触发doall
(->>
(concat
(lazy-positive-numbers 10)
(lazy-positive-numbers 20)
(lazy-positive-numbers 30))
(doall)
(take 15))
;executing 10
;executing 20
;executing 30
;executing 11
;executing 12
;executing 13
;executing 14
;executing 15
;executing 16
;executing 17
;executing 18
;executing 19
;executing 20
;executing 21
;executing 21
;executing 22
;executing 23
;executing 24
;executing 25
;executing 26
;executing 27
;executing 28
;executing 29
;executing 30
;executing 31
;executing 31
;executing 32
;executing 33
;executing 34
;executing 35
;executing 36
;executing 37
;executing 38
;executing 39
;executing 40
;executing 41
;(10 11 12 13 14 15 16 17 18 19 20 20 21 22 23)
如果我们错过doall
所有expresssions将不进行评估。
第8课—警惕Clojure核心功能— map != mapv
, filter != filterv
等
第9课-使用 doall
缩短懒惰并评估所有
总之,“懒惰”,您真棒。
以上课程来自Swym Corporation的内部开发/工程演讲。 演讲对Clojure的功能模式进行了更广泛的概述,我希望在我的下一篇文章中对此进行介绍。
特别感谢Saumitra , Supritha和Shivam帮助使这篇文章看起来不错。 希望您喜欢阅读它,就像我们喜欢一起阅读它一样。 请随意输入您的想法。 感谢您的意见和问题! :)
PS:也许最好的事情? 不,谢谢。 这是“希望”所采取的。 我不是那个意思。 安迪·杜弗雷斯(Andy Dufresne)做过:)
参考资料
- https://clojuredocs.org/clojure.core/lazy-seq-有关如何编写“惰性生产者”的大量示例
- https://clojure.org/reference/lazy
- http://clojure-doc.org/articles/language/laziness.html
- https://medium.com/lazy-eval/lazy-seq-in-clojure-da06f6d35971
- https://stuartsierra.com/2015/08/25/clojure-donts-lazy-effects
- https://github.com/danielmiladinov/joy-of-clojure/blob/master/src/joy-of-clojure/chapter6/laziness.clj
- https://noobtuts.com/clojure/being-lazy-in-clojure
- https://practicalli.github.io/clojure/thinking-functionally/lazy-evaluation.html
- http://www.thesoftwaresimpleton.com/blog/2014/09/08/lazy-seq/
翻译自: https://hackernoon.com/clojure-lessons-from-laziness-252ca7fc4fa7
clojure