Clojure 和 ClojureScript 的 REPL 产生不同的输出结果

4
使用以下深度优先搜索的递归定义,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
;; => ((() ()) (()) ())

2
在Clojure 1.8和1.9-alpha14中,我末尾没有:f,而且我的括号与你的cljs括号相同。 - Josh
很奇怪,我正在使用Clojure 1.8.0和ClojureScript 1.9.229。 - mac
1个回答

6
Clojure的for生成一个惰性序列。所有递归dfs调用的实际评估只有在你的REPL需要打印函数输出,即((() ()) (()) ())时才会触发。如果你评估(do (dfs graph :a) nil),你只会得到:a的输出。

现在,Clojure的惰性序列为了效率是以32个元素一组进行评估的。因此,当REPL(通过str函数)评估第一个元素的惰性序列时,顶层的for(应该打印:b)会被评估,该序列的其他元素也会被评估,你会在子节点序列被评估之前就得到:c:e的输出(它们也是惰性的)。

相比之下,Clojurescript的惰性序列不是分块的(LazySeq 没有实现 IChunkedSeq),而是一个一个地被评估,因此当返回值递归转换为字符串时,所有内容都按深度优先顺序进行评估。
举个例子 - 在Clojure和CLJS的REPL中尝试(first (for [i (range 300)] (do (println "printing:" i) i))) - 你会得到32个数字在Clojure中打印,而只有一个数字在CLJS中打印。
如果您希望更好地保证评估顺序,可以使用doseq代替for或将for包装在doall中。
希望这可以帮助您。
旁注:就像@Josh一样,在Clojure 1.8中我没有得到:f,并且括号与cljs输出相同 - 这真的很奇怪...

我不确定你当前想要如何使用DFS的结果。如果你想使用副作用,即将所有节点打印到控制台,请使用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))))))

这将深度优先打印所有节点到控制台。如果您想获取遍历作为返回值,请使用for,但请确保您实际上返回了有意义的值:
(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)) - 你可以将其展开以获得遍历。你还将获得惰性的好处。

谢谢你的回答。我觉得评估顺序会受到分块的影响,这对我来说有点疯狂。另外,当我只打印v时,为什么其他元素也会输出到REPL中呢?如果我删除println调用,就什么都不会打印出来。 - mac
问题在于您混合了具有副作用的代码和惰性代码。在您的情况下,评估的顺序(以及事实本身)不仅取决于分块,还取决于您的 REPL 是否将函数“((() ()) (()) ())”的返回值输出到控制台 - 如果您考虑一下,这本身就很疯狂。如果您想要将结果输出到控制台,则可以使用命令式代码,如果您想要结果作为返回值,则可以使用函数/惰性代码(请参见我上面帖子中的几个示例)。但是混合这两种方法并不是正确的Clojure方式。 - Aleph Aleph

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接