Clojure 程序在调试时工作正常,在 repl 中失败

Clojure program works fine when debugged, fails in repl

我正在学习core.async并编写了一个简单的生产者消费者代码:

(ns webcrawler.parallel
  (:require [clojure.core.async :as async
             :refer [>! <! >!! <!! go chan buffer close! thread alts! alts!! timeout]]))

(defn consumer
  [in out f]
  (go (loop [request (<! in)]
        (if (nil? request)
          (close! out)
          (do (print f)
            (let [result (f request)]
              (>! out result))
              (recur (<! in)))))))

(defn make-consumer [in f]
  (let [out (chan)]
    (consumer in out f)
    out))

(defn process
  [f s no-of-consumers]
  (let [in (chan (count s))
        consumers (repeatedly no-of-consumers #(make-consumer in f))
        out (async/merge consumers)]
    (map #(>!! in %1) s)
    (close! in)
    (loop [result (<!! out)
           results '()]
      (if (nil? result)
        results
        (recur (<!! out)
               (conj results result))))))

当我通过 Emacs 苹果酒提供的调试器中的 process 函数介入时,这段代码工作正常。

(process (partial + 1) '(1 2 3 4) 1)
(5 4 3 2)

但是,如果我 运行 它本身(或在调试器中点击继续),我得到一个空结果。

(process (partial + 1) '(1 2 3 4) 1)
()

我的猜测是,在第二种情况下,出于某种原因,生产者在退出之前不会等待消费者,但我不确定为什么。感谢您的帮助!

我敢打赌,这是由 map 的懒惰行为造成的,而这一行会产生副作用:

(map #(>!! in %1) s)

因为您从未明确使用结果,所以它永远不会 运行s。将其更改为使用 mapv,这是严格的,或者更准确地说,使用 doseq。永远不要使用 map 到 运行 的副作用。它旨在懒惰地转换列表,滥用它会导致这样的行为。

那么为什么它在调试时可以正常工作?我猜是因为调试器强制将评估作为其操作的一部分,这掩盖了问题。

问题是您对 map 的调用是惰性的,并且不会 运行 直到有人要求结果。在您的代码中没有这样做。

有2种解决方案:

(1) 使用eager函数mapv:

(mapv #(>!! in %1) items)

(2) 使用 doseq,它用于副作用操作(例如将值放在通道上):

(doseq [item items]
  (>!! in item))

两者都可以工作并产生输出:

(process (partial + 1) [1 2 3 4] 1) => (5 4 3 2)

P.S。您在 (defn consumer ...)

中有一个调试语句
(print f)

在输出中产生大量噪音:

<#clojure.core$partial$fn__5561 #object[clojure.core$partial$fn__5561 0x31cced7
"clojure.core$partial$fn__5561@31cced7"]>

背靠背重复5次。你可能想避免这种情况,因为打印功能 "refs" 对人类来说毫无用处 reader.

此外,调试打印输出一般应使用 println,这样您就可以看到每个打印输出的开始和结束位置。

正如您可以从文档字符串中读取的那样 map returns 一个惰性序列。而且我认为最好的方法是使用dorun。这是来自 clojuredocs 的示例:

;;map a function which makes database calls over a vector of values 
user=> (map #(db/insert :person {:name %}) ["Fred" "Ethel" "Lucy" "Ricardo"])
JdbcSQLException The object is already closed [90007-170]  org.h2.message.DbE
xception.getJdbcSQLException (DbException.java:329)

;;database connection was closed before we got a chance to do our transactions
;;lets wrap it in dorun
user=> (dorun (map #(db/insert :person {:name %}) ["Fred" "Ethel" "Lucy" "Ricardo"]))
DEBUG :db insert into person values name = 'Fred'
DEBUG :db insert into person values name = 'Ethel'
DEBUG :db insert into person values name = 'Lucy'
DEBUG :db insert into person values name = 'Ricardo'
nil