デバッグの第一歩!Emacs Lisp関数をトレースする方法

Emacs Lispデバッグをしているとき、特定の関数呼び出しをトレースしたいことがあります。 関数呼び出しのトレースとは、呼び出した関数の引数の値と、返り値をチェックすることです。 もちろん、関数が呼び出されたかどうかもわかります。

トレースの実現にはadvice機能を使っています。

M-x trace-functionでトレースする

M-x trace-functionは、特定の関数をトレースします。

このコマンドは、トレース対象関数と出力バッファを聞いてきます。 出力バッファはデフォルトで*trace-output*なので、そのままにしておくのが無難です。 複数の関数をトレースする際に出力がごちゃごちゃになるのが嫌ならば別のバッファ名にしておけばいいです。

その後でトレース対象関数を呼び出したら、トレース結果を表示します。

M-x trace-function-backgroundで黙ってトレースする

M-x trace-function-backgroundは、M-x trace-functionと同じ働きですが、結果を表示しません。 ウィンドウ構成を変更したくない場合はM-x trace-function-backgroundを使います。

M-x untrace-functionで特定の関数のトレースを無効にする

M-x untrace-functionは、特定のトレース対象関数のトレースを無効にします。

M-x untrace-allですべてのトレースを無効にする

M-x untrace-allは、すべてのトレース対象関数のトレースを無効にします。 面倒なので、M-x untrace-allを使う方が多くなるでしょう。

単純な例

(defun f (x) (+ x 3))
(defun g (x) (+ (f x) 7))
(g 10)                                  ; => 20

この例でfに対してM-x trace-functionを実行して (g 10) を呼び出したら、以下の出力が得られます。


======================================================================
1 -> f: x=10
1 <- f: 13
factの例

(defun fact (n)
   (if (= n 0) 1
     (* n (fact (1- n)))))
(fact 3)                                ; => 6

次は再帰呼び出しの例です。factに対してM-x trace-functionを実行すると、以下の出力が得られます。 このように、左の数字は再帰レベルを表していることがわかります。


======================================================================
1 -> fact: n=3

2 -> fact: n=2
3 -> fact: n=1
4 -> fact: n=0
4 <- fact: 1
3 <- fact: 1
2 <- fact: 2

1 <- fact: 6

trace-function-regexpとtrace-function-background-regexpを定義することでまとめてトレースする

(defun trace-function-regexp (regexp &optional buffer)
  (interactive
   (list
    (read-string "Trace function regexp: ")
    (read-buffer "Output to buffer: " trace-buffer)))
  (mapc 'trace-function (apropos-internal regexp 'fboundp)))
(defun trace-function-background-regexp (regexp &optional buffer)
  (interactive
   (list
    (read-string "Trace function background regexp: ")
    (read-buffer "Output to buffer: " trace-buffer)))
  (mapc 'trace-function-background (apropos-internal regexp 'fboundp)))

これらの小さなコマンドを定義することで、まとめて関数をトレースすることができます。 ログが増えますが、開発中には役立つでしょう。

(defun hoge-1 (x) (1+ x))
(defun hoge-2 (x) (* 2 (hoge-1 x)))
(hoge-2 33)
(hoge-1 4)

この例において M-x trace-function-regexp ^hoge- を実行すると、以下の出力が得られます。


======================================================================
1 -> hoge-2: x=33

2 -> hoge-1: x=33
2 <- hoge-1: 34

1 <- hoge-2: 68
======================================================================
1 -> hoge-1: x=4
1 <- hoge-1: 5

小さい関数を書け

プログラミングしていくにおいては、やはり小さい関数を書いていくことが重要になります。

第1に、関数を小さくしていくと、もちろんトレースしやすくなります。 細かい単位で値の受け渡しがわかるから、バグが見つけやすくなりるのです。 そして、おかしい関数に焦点をしぼることができます。

第2に、関数が小さいとテストがしやすくなります。

第3に、特定の処理のかたまりを関数化しておくと、処理に名前をつけることができます。 処理を表す適切な関数名をつけておくことで、コードを読んだだけでその部分が何をやっているのかが明らかです。 その部分に対してはコメントが不要になります。

第4に、その関数が再利用可能になります。

こんなことは常識だと思いますが、Emacs Lispのコードを見ていると、守れていないものがいかに多いことか。 100行超の関数はざらに見かけるし、読みにくさには閉口するばかりです。 機能拡張をしたくなっても、関数が大きい固まりになっているので、コピー&ペーストせざるを得ないこともままあります。 このときのもどかしさといったら…泣けてきます。

ちなみに僕は1行の処理であっても関数としてまとめるのを好みます。 処理に名前をつけ、読みやすくするのが目的です。