【问题标题】:Clojure "concat" not being lazyClojure“concat”并不懒惰
【发布时间】:2019-08-14 13:00:35
【问题描述】:

我一直在测试concat 的行为。

文档字符串说:

返回一个惰性序列,表示元素的串联 提供的 colls。

然而,concat 似乎并没有因为它的论点而表现得很懒惰。相反,我们观察到通常的热切评估。这不是我所期望的。

观察:

这里是生成二叉树的简单代码,该二叉树包含来自“The Joy of Clojure, 2nd edition”, p. 中的整数。 208:

; we have a binary tree based on records, holding a val and having left
; and right subtrees

(defrecord TreeNode [val left right])

; xconj basically is insertion sort; inserts value v into tree t. 
; + The code in JoC is more compact; here, "explicited" for readability.

(defn xconj [t v]
   (cond
      (nil? t)            (TreeNode. v nil nil)
      (< v (get t :val))  (TreeNode. (get t :val)
                                     (xconj (get t :left) v)
                                     (get t :right))
      :else               (TreeNode. (get t :val)
                                     (get t :left)
                                     (xconj (get t :right) v))))

; Convert a tree into a seqs (in-order traversal, so the seq will spit 
; out the integers in order sorted ascending).
; Returns a lazy seq as "concat" returns clojure.lang.LazySeq
; + The code in JoC is more compact; here, "explicited" for readability.

(defn xseq [t]
   (when (some? t)
      (concat (xseq (get t :left))
              [ (get t :val) ]
              (xseq (get t :right)))))

; "xseq" is a bit mute; add some printout to probe behaviour (watching
; out to not destroy laziness when doing so)

(defn xseq-p1 [t k]
   (if (nil? t) (println k "▼" "⊥") (println k "▼" (get t :val)))
   (when (some? t)
      (concat (xseq-p1 (get t :left) (str k "[" (get t :val) "]" "◀"))
              [ (get t :val) ]
              (xseq-p1 (get t :right) (str k "[" (get t :val) "]" "▶")))))

; create a tree for testing

(def ll (reduce xconj nil [3 5 2 4 6]))

现在,查询xseq-p1返回的值的类型,显示遍历了整棵树?!

[3]◀[2]▶ ▼ ⊥ 表示找到 3,向左,找到 2,向右,现在为零

(type (xseq-p1 ll ""))
; ▼ 3
; [3]◀ ▼ 2
; [3]◀[2]◀ ▼ ⊥
; [3]◀[2]▶ ▼ ⊥
; [3]▶ ▼ 5
; [3]▶[5]◀ ▼ 4
; [3]▶[5]◀[4]◀ ▼ ⊥
; [3]▶[5]◀[4]▶ ▼ ⊥
; [3]▶[5]▶ ▼ 6
; [3]▶[5]▶[6]◀ ▼ ⊥
; [3]▶[5]▶[6]▶ ▼ ⊥
; clojure.lang.LazySeq

xseq 变得懒惰需要在concat 前面多加一个lazy-seq

(defn xseq-p2 [t k]
   (if (nil? t) (println k "▼" "⊥") (println k "▼" (get t :val)))
   (when (some? t)
      (lazy-seq
      (concat (xseq-p2 (get t :left) (str k "[" (get t :val) "]" "◀"))
              [ (get t :val) ]
              (xseq-p2 (get t :right) (str k "[" (get t :val) "]" "▶"))))))

现在它很懒:

(type (xseq-p2 ll ""))
; ▼ 3
; clojure.lang.LazySeq

(take 2 (xseq-p2 ll ""))
; ▼ 3
; ([3]◀ ▼ 2
; [3]▶ ▼ 5
; [3]◀[2]◀ ▼ ⊥
; [3]◀[2]▶ ▼ ⊥
; 2 3)

这是预期的吗?

附言

另一种方法是延迟两个下降(或仅向右下降)。两种下降都懒惰,xseq-p3 甚至比xseq-p1 更懒惰:

(defn xseq-p3 [t k]
   (if (nil? t) (println k "▼" "⊥") (println k "▼" (get t :val)))
   (when (some? t)
      (let [ left   (get t :left)
             v      (get t :val)
             right  (get t :right)
             l-seq  (lazy-seq (xseq-p3 left  (str k "[" v "]" "◀")))
             r-seq  (lazy-seq (xseq-p3 right (str k "[" v "]" "▶"))) ]
         (concat l-seq [v] r-seq))))

(type (xseq-p3 ll ""))
; ▼ 3
; clojure.lang.LazySeq

(take 2 (xseq-p3 ll ""))
; ▼ 3
; ([3]◀ ▼ 2
; [3]◀[2]◀ ▼ ⊥
; [3]◀[2]▶ ▼ ⊥
; 2 3)

【问题讨论】:

  • lazy-cat 可能会有所帮助。
  • @glts 啊,是的,在xseq-p1 中使用lazy-cat 而不是cat 使其表现得像xseq-p3!那是一场胜利。但是,concat 的“懒惰”是什么?我想说,只有当它的论点一开始是懒惰的时候,它才会表现得懒惰。

标签: clojure lazy-evaluation


【解决方案1】:

作为参数传递给 Clojure 函数的任何表达式都会被急切地求值,因此函数代码只能看到一个值。它可以是原始的(例如42)或内置的(例如"hello")或复合值(例如[42 "hello" {:a 1 :b 2}])。该值可能是像(range) 产生的惰性序列。

请注意,如果您键入 (take 3 (range))take 函数将看不到 (range) 部分。相反,它看起来像(take 3 &lt;lazy-seq-produced-by-range&gt;)。所以表达式(range) 中的函数调用被急切 求值,它产生的惰性序列被传递给take 表达式。

如果 arg 是惰性序列,则函数本身不会意识到这一点。您可以使用println 等检测生成惰性序列以观察时序,但这不会影响函数如何通过(first arg)(nth arg 3) 等使用值。通常,您只需关心函数如何生成惰性结果,也许关心它消耗了输入序列的多少元素(惰性或非惰性)。

您还应该知道,为了提高效率,Clojure 中的大多数惰性序列都以长度为 32 的块运行。这意味着惰性序列实际上可以完成比预期更多的工作。例如,假设您只想从惰性序列中消耗 3 个“昂贵”的项目。由于在您请求第一个项目时分块通常会生成 32 个项目,因此您做了不必要的和不需要的额外工作。

我通常避免惰性序列,因为它们无法预测 then 何时运行以及序列中将实现多少项。因此,我总是使用mapvfilterv 和朋友,并经常用(vec ...) 包装其他东西(例如,我有自己的非懒惰forv)。我只在输入/输出真正“大”时使用惰性序列(例如处理大型数据库表中的每一行)。

【讨论】:

  • 关于分块的警告是好的,但我不会说“大多数”惰性序列是分块的。有些是,所以如果你关心分块,你需要小心所有的序列,但是有很多方法可以产生不分块的惰性序列。
  • 谢谢。您说“所有 clojure 函数都急切地评估它们的参数(即在将参数传递给函数代码之前)。”但是这是错误的?!在xseq-p3 中,lazy-seq 后面的递归调用显然是“暂停”:虽然返回了底层类型 clojure.lang.LazySeq 的东西,但树的顺序遍历还没有真正开始,遍历只是触及根节点,打印▼ 3。仅当实际需要第一个元素时,返回的“thunk”中的内容才会被“步进”。
猜你喜欢
  • 2010-12-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多