Идиоматический clojure для отчетов о прогрессе?

Как мне отслеживать ход отображаемой функции в 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)

Существуют ли другие (лучшие) способы достижения этого эффекта?

Есть ли какие-то подводные камни в том, что я делаю? (Я думаю, что я сохраняю лень и не удерживаю голову, например.)

Ответ 1

Самое замечательное в 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))

Ответ 2

Я бы, вероятно, выполнил отчет в агенте. Что-то вроде этого:

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

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

Ответ 3

Я не знаю никаких существующих способов сделать это, возможно, было бы неплохо просмотреть документацию 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))

Вы направляетесь в правильном направлении, хотя этот пример слишком прост. Это дало мне представление о более обобщенной версии вашей функции отслеживания отчетов. Эта функция будет использовать функцию, подобную карте, отображаемую функцию, функцию отчета и набор коллекций (или начальное значение и сбор для тестирования).

(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)))

Секундомер часть существует только для использования с уменьшением, которое не обязательно возвращает последовательность. С помощью этой функции мы можем переписать Пример:

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)

И даже функция уменьшения:

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

Ответ 4

У меня была эта проблема с некоторыми медленными приложениями (например, 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

API-документы для пространства имен tupelo.misc можно найти здесь.