Emacs Lispで時間がかかる処理をするときに進捗状況を報告する

動機

時間のかかる処理をしていると、いつまで待たされるのかわからなくなります。 このとき、進捗状況を表示してくれると安心です。

マニュアルより Progress - GNU Emacs Lisp Reference Manual

使い方

単純な数値ループ: (dotimes (変数 回数) 処理〜)

決まった回数(N)だけループするにはdotimesを使います。 以下の例では、メッセージを表示し、500個カウント(1カウントあたり0.01秒なので)します。 、5秒後(0.01×500)にdoneと表示します。


(progn
(message "Collecting some mana for Emacs...")
(dotimes (k 500)
(sit-for 0.01))
(message "Collecting some mana for Emacs...done"))
進捗状況付き数値ループ: (dotimes-with-progress-reporter (変数 回数) メッセージ 処理〜)

これを進捗状況付きにするのは簡単です。 dotimesの変数設定の次の引数にメッセージ文字列を置くだけです。


(dotimes-with-progress-reporter (k 500)
"Collecting some mana for Emacs..."
(sit-for 0.01))
もっと細かく制御する

それでは、進捗状況表示機能の関数を細かく見ていきましょう。 dotimes-with-progress-reporterを分解してみると、次のようになります。


(let ((progress-reporter
(make-progress-reporter "Collecting mana for Emacs..."
0 500)))
(dotimes (k 500)
(sit-for 0.01)
(progress-reporter-update progress-reporter k))
(progress-reporter-done progress-reporter))
(make-progress-reporter メッセージ &optional 最小値 最大値 初期値 略〜)

make-progress-reporter関数は、エコーエリアにメッセージを表示して、進捗状況オブジェクトを作成します。 初期値を設定することで、カウントの途中から始めることができます。

(progress-reporter-update 進捗オブジェクト 値)

「値」を増やすことで進捗状況を進めます。 それに伴い、進捗状況メッセージも変化します。

(progress-reporter-done 進捗オブジェクト)

処理を終えます。 エコーエリアには、進捗状況オブジェクトで示されたメッセージに続いて「done」と表示されます。

応用

たくさんのファイルについて処理をする

実際、数値ループよりもリストでループすることが多いと思います。 たとえば、たくさんのファイルにたいして処理をするようなケースです。 /etc以下のファイルを徐々に表示する例ではこうなります。

(let ((files (directory-files "/etc"))
      file)
  ;; 処理後にバッファを表示する
  (with-output-to-temp-buffer " *test*"
    (dotimes-with-progress-reporter (k (length files))
        "Listing files..."
      (setq file (car files))
      (setq files (cdr files))
      (print file)
      (sit-for 0.001))))

はい、美しくありません。 本当は「それぞれのファイルに対して」と表現したいのに、「ファイルリストのcarとcdrをいじって」という低レベルな表現が含まれているからです。


(with-output-to-temp-buffer " *test*"
(loop with files = (directory-files "/etc")
with progress-reporter = (make-progress-reporter "Listing files..." 0 (length files))
for file in files
for k from 0
do
(print file)
(sit-for 0.001)
(progress-reporter-update progress-reporter k)
finally (progress-reporter-done progress-reporter)))

loopを使ったら逆に複雑になってしまいました。

進捗状況なしならば、シンプルにdolistで書くことができます。


(with-output-to-temp-buffer " *test*"
(dolist (file (directory-files "/etc"))
(sit-for 0.001)
(print file)))

要は なんでdolist版がないの? ということです。

(dolist-with-progress-reporter (変数 リスト値) メッセージ 処理〜)

なければ作ってしまおうということで、 dotimes-with-progress-reporter からパクってきました。

(defmacro dolist-with-progress-reporter (spec message &rest body)
  (declare (indent 2) (debug ((symbolp form &optional form) form body)))
  (let* ((end-sym (make-symbol "--dotimes-end-sym--"))
         (pr-sym (make-symbol "--dotimes-pr-sym--"))
         (lst-sym (make-symbol "--dotimes-lst-sym--"))
         (count-sym (make-symbol "--dotimes-count-sym--"))
         (start 0)
         (lst (nth 1 spec)))
    `(let* ((,end-sym (length ,lst))
            (,count-sym ,start)
            (,lst-sym ,lst)
            (,pr-sym (make-progress-reporter ,message ,start ,end-sym)))
       (while (< ,count-sym ,end-sym)
         (setq ,(car spec) (car ,lst-sym))
         (setq ,lst-sym (cdr ,lst-sym))
	 ,@body
	 (progress-reporter-update ,pr-sym
				   (setq ,count-sym (1+ ,count-sym))))
       (progress-reporter-done ,pr-sym)
       nil ,@(cdr (cdr spec)))))

さっそく使ってみましょう。dotimes同様すっきりします。


(with-output-to-temp-buffer " *test*"
(dolist-with-progress-reporter (file (directory-files "/etc"))
"Collecting mana for Emacs..."
(sit-for 0.001)
(print file)))

返信

To:id:peccu

はてな記法に化けていましたね。御指摘感謝です。