Clojure中用于进度报告的惯用语?

34

在Clojure中如何监控映射函数的进度?

在命令式语言中处理记录时,我经常打印一条消息来指示已经完成了多少工作,例如每处理1000个记录报告一次。实际上这是计数循环迭代。

我想知道在Clojure中,当我将一个函数映射到记录序列时,我可以采取哪些方法来监控进度。在这种情况下,打印消息(甚至保持进度计数)似乎基本上是副作用。

我目前想到的方案如下:

(defn report
  [report-every val cnt]
  (if (= 0 (mod cnt report-every))
    (println "Done" cnt))
    val)

(defn report-progress
  [report-every aseq]
  (map (fn [val cnt] 
          (report report-every val cnt)) 
       aseq 
       (iterate inc 1)))
例如:
user> (doall (report-progress 2 (range 10)))
Done 2
Done 4
Done 6
Done 8
Done 10
(0 1 2 3 4 5 6 7 8 9)

有没有其他更好的方法来实现这种效果?

我所做的是否存在任何潜在问题?(我认为我保留了lazy evaluation并且没有持有head,但是还有其他问题吗?)

4个回答

33

Clojure的伟大之处在于你可以将报告附加到数据本身,而不是执行计算的代码上。这样可以将逻辑上不同的部分分离开来。下面是我在misc.clj中使用的一段代码块,在几乎每个项目中都需要用到:

(defn seq-counter 
  "calls callback after every n'th entry in sequence is evaluated. 
  Optionally takes another callback to call once the seq is fully evaluated."
  ([sequence n callback]
     (map #(do (if (= (rem %1 n) 0) (callback)) %2) (iterate inc 1) sequence))
  ([sequence n callback finished-callback]
     (drop-last (lazy-cat (seq-counter sequence n callback) 
                  (lazy-seq (cons (finished-callback) ())))))) 

然后将报告器围绕着您的数据包裹起来,然后将结果传递给处理函数。

(map process-data (seq-counter inc-progress input))

1
我认为我在做类似的事情,将报告附加到一个序列中,可以对其中任何内容进行操作。我原本想将其附加到结果序列,但同样也可以是输入序列。不过你的代码更好。我还没有使用回调来处理报告消息(或更一般的函数),我是为每个值调用报告函数。 - Alex Stoddard
1
你有分享misc.clj的任何地方吗?我肯定会受益于看到其他有用的东西,比如序列计数器的其他想法和实现。 - Alex Stoddard
1
是的,它确实与您最初的示例相同,我在没有正确理解问题的情况下有点快地说出了“哦,那在misk.clj中”。http://code.google.com/p/cryptovide/source/browse/src/com/cryptovide/misc.clj。 - Arthur Ulfeldt
似乎应用程序示例中有错别字。我正在扩展示例以探索我的理解。为了利用解决方案领域中的格式化功能,我必须在上面的答案中添加我的修复。同时,我还发现了一个意外输出的惊喜。请帮忙审查一下,看看我是否有什么误解。谢谢。 - Yu Shen
1
我将上面解决方案中的概念概括成一个库,以便于将其轻松地添加到项目中。希望这能帮助到某些人。 https://github.com/tmountain/seq-peek。 - tmountain

6

我可能会在代理程序中执行报告。就像这样:

(defn report [a]
  (println "Done " s)
  (+ 1 s))

(let [reports (agent 0)]
  (map #(do (send reports report)
            (process-data %))
       data-to-process)

1
这是一个有趣的方法。奇怪的是,如果我在emacs中使用slime-mode,报告不会显示在我的repl中,但在普通的repl中会打印出来。 - Alex Stoddard
1
经过进一步的思考,我可以在发送给代理的函数中递增事物。进度的打印可以是一个常规函数,在REPL中访问代理的状态。 - Alex Stoddard
1
其实这是个好观点。事实上,如果你正在更新GUI,你可能必须在主线程中执行它(或者将其推迟到主线程中,使用dispatchLater等方式)。 - user21037

4

我不知道现有的任何做法,也许浏览clojure.contrib文档以查看是否已经有了一些东西是个好主意。与此同时,我已经看了一下你的例子并稍微梳理了一下。

(defn report [cnt]
  (when (even? cnt)
    (println "Done" cnt)))

(defn report-progress []
  (let [aseq (range 10)]
    (doall (map report (take (count aseq) (iterate inc 1))))
    aseq))

尽管这个例子过于简单,但你正在朝着正确的方向前进。这启发了我关于一个更加通用化版本的report-progress函数的想法。该函数将会接受一个类似于map的函数、被映射的函数、一个报告函数以及一组集合(或者一个种子值和一个用于测试reduce的集合)。

(defn report-progress [m f r & colls]
  (let [result (apply m
                 (fn [& args]
                   (let [v (apply f args)]
                     (apply r v args) v))
                 colls)]
    (if (seq? result)
      (doall result)
      result)))

seq?部分仅用于与reduce一起使用,因为reduce不一定返回一个序列。使用这个函数,我们可以像这样重写你的示例:

user> 
(report-progress
  map
  (fn [_ v] v)
  (fn [result cnt _]
    (when (even? cnt)
      (println "Done" cnt)))
  (iterate inc 1)
  (range 10))

Done 2
Done 4
Done 6
Done 8
Done 10
(0 1 2 3 4 5 6 7 8 9)

测试过滤功能:

user> 
(report-progress
  filter
  odd?
  (fn [result cnt]
    (when (even? cnt)
      (println "Done" cnt)))
  (range 10))

Done 0
Done 2
Done 4
Done 6
Done 8
(1 3 5 7 9)

甚至包括reduce函数:

user> 
(report-progress
  reduce
  +
  (fn [result s v]
    (when (even? s)
      (println "Done" s)))
  2
  (repeat 10 1))

Done 2
Done 4
Done 6
Done 8
Done 10
12

1
我认为你没明白我在用 'doall' 做什么(对于糟糕和不清晰的代码,我很抱歉)。我只是使用 doall 在 repl 中进行测试报告,以强制整个序列的报告而不是处理(否则它将被懒惰地评估)。 doall 不是我尝试报告函数或预期序列处理的一部分。 - Alex Stoddard

-1

我曾经遇到一些运行缓慢的应用程序(例如数据库ETL等),但我通过添加函数(tupelo.misc/dot ...)到tupelo库来解决了这个问题。示例:

(ns xxx.core 
  (:require [tupelo.misc :as tm]))

(tm/dots-config! {:decimation 10} )
(tm/with-dots
  (doseq [ii (range 2345)]
    (tm/dot)
    (Thread/sleep 5)))

输出:

     0 ....................................................................................................
  1000 ....................................................................................................
  2000 ...................................
  2345 total

tupelo.misc 命名空间的 API 文档可以在这里找到


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