Clojure 和 ClojureScript REPL 产生不同的输出

Clojure and ClojureScript REPL produce different output

使用深度优先搜索的以下递归定义 Clojure (JVM) 和 ClojureScript(使用浏览器连接的 repl 和 lumo 进行测试)REPL 产生两个不同的输出,即打印节点的顺序不同,Clojure REPL产生重复的 :f。 ClojureScript 命令是我所期望的行为。这是为什么?

代码:

(defn dfs
  ([g v] (dfs g v #{}))
  ([g v seen]
   (println v)
   (let [seen (conj seen v)]
     (for [n (v g)]
       (if-not (contains? seen n)
         (dfs g n seen))))))

(def graph {:a [:b :c :e]
            :b [:d :f]
            :c [:g]})

(dfs graph :a)

Cloure REPL 输出:

:a
:b
:c
:e
:d
:f
:g
:f
;; => ((() ()) (()) (()))

CLojureScript REPL 输出:

:a
:b
:d
:f
:c
:g
:e
;; => ((() ()) (()) ())

Clojure 的 for 生成惰性序列。所有循环 dfs 调用的实际评估仅由您的 REPL 触发,因为它需要打印函数的输出,即 ((() ()) (()) ())。如果你计算 (do (dfs graph :a) nil),你只会打印 :a

现在,Clojure 的惰性序列 evaluated in chunks of size 32 提高了效率。因此,当 REPL(通过 str 函数)评估第一个元素惰性序列的顶层 for(应该打印 :b)时,该序列的其他元素也会被评估,你在评估子节点的序列(也是惰性的)之前打印 :c:e

相比之下,Clojurescript 的惰性序列不分块(LazySeq does not implement IChunkedSeq)并且是逐个计算的,所以当 return 值被递归转换为字符串时,一切都会计算深度优先。

为了说明这一点 - 在 Clojure 和 CLJS 的 REPL 中尝试 (first (for [i (range 300)] (do (println "printing:" i) i))) - 你将在 clojure 中打印 32 个数字,在 CLJS 中只打印一个数字。

如果你想更好地保证评估的顺序,你可以使用doseq代替for或者将for包裹在doall中。

希望这对您有所帮助。

旁注:就像@Josh,我最终在 Clojure 1.8 中没有得到 :f,并且括号与 cljs 输出相同 - 这真的很奇怪......

我不确定我是否了解您目前想要如何使用 DFS 的结果。如果你想使用副作用,我。 e.打印所有节点到控制台,使用doseq确保遍历:

(defn dfs-eager
  ([g v] (dfs-eager g v #{}))
  ([g v seen]
   (println v)
   (let [seen (conj seen v)]
     (doseq [n (v g)]
       (if-not (contains? seen n)
         (dfs-eager g n seen))))))

这会将所有节点打印到控制台,深度优先。如果您想将遍历作为 return 值,请使用 for 但请确保您实际上 return 是一个有意义的值:

(defn dfs-lazy
  ([g v] (dfs-lazy g v #{}))
  ([g v seen]
   (cons v
         (let [seen (conj seen v)]
           (for [n (v g)]
             (if-not (contains? seen n)
               (dfs-lazy g n seen)))))))

您将得到一个嵌套列表 (:a (:b (:d) (:f)) (:c (:g)) (:e)) - 然后您可以将其展平以进行遍历。你也会得到懒惰的好处。