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
はてな記法に化けていましたね。御指摘感謝です。