glider-gun's Blog

何か書きます

Debugging Lisp Part 1: 再コンパイル

このエントリーは、著者 Michael Malis 氏の許可をいただき、Common Lispのデバッグに関する連載 http://malisper.me/category/debugging-common-lisp/ を翻訳するものです。

目次: 第1回 第2回 第3回 第4回 第5回


このエントリーはCommon Lispのデバッギング、特にEmacs、Slime、SBCLを用いた場合に関する連載の第1回です。Common Lispを知らない人でも少なくとも追うことはでき、Common Lispのデバッガが提供するものがいかにパワフルかわかるはずです。このエントリー群はNathan Marzに書くよう頼まれたものです。彼はCommon Lispをデバッグする多くのツール群がクールだと考えたのです。

Common Lispのデバッグを始めるに当たって最初に、あなたのLispで最適化の指定をする必要があります。Common Lispでは、コードのコンパイルにあたって何の項目を重要視すべきかを設定することができます。項目としてはたとえば実行速度、メモリ量、コンパイル速度、そしてデバッグがあります。以下のコードを走らせなければ、このエントリーで以下示すコードのほとんどは動作しません。

1
2
3
4
5
CL-USER> (declaim (optimize (debug 3)))
NIL

CL-USER> (your-program)
...

デバッグを優先してコンパイルすることにしたので、実行時に色々なことができるようになりました。 以下ではベテランのCommon Lisp開発者(トムとしましょう)に登場してもらい、彼がどのようにデバッグを行い、バグを含んだ関数を修正するかを示して行きます。トムの手元に、フィボナッチ関数を計算する次のようなコードがあるとします。

1
2
3
4
5
(defun fib (n)
  (if (<= 0 n 1)
      (/ 1 0)
      (+ (fib (- n 1))
         (fib (- n 2)))))

このコードには問題が一つあります。正しくないということです!base caseでnを返すはずが、ゼロ除算を行ってしまいます。トムが10番目のフィボナッチ数を計算しようとするとデバッガウィンドウが現れます。エラーが通知されたからです。

トムはデバッガに入ったことに気付いて、何がまずかったのか考えます。バグを見つけるため、トムは関数にブレークポイントを挿入することにしました1。Common Lispでは、ブレークポイントは関数として実装されており、"break"という名前です2。ブレークポイントを挿入するため、トムは fib の最初に break の呼び出しを追加しました(訳注: 関数定義のコードにカーソルを置いてC-c C-cを押すことでその関数を再コンパイルします)。ブレークポイントを追加したトムは、次に一つのフレームのところにカーソルを持って行ったのち、'r'キーを押しました。これでそのフレームから実行を再開(リスタート)できます。トムは n が 3 だったフレームをリスタートすることにしました。

フレームをリスタートすることで、トムは要するにそのフレームまで時間を巻き戻すことができます。リスタートをすると、デバッガはトムが挿入したブレークポイントにただちにぶつかります。トムはそこから’s'キーを押してプログラムを1ステップずつ実行します。最終的に彼は base case の実装が正しくないためにエラーが出ていることに気付きました。

さて、エラーの原因がわかりました。ブレークポイントを挿入したときと同じようにして、トムがコードを修正します。base caseを n で置き換えて、先ほど挿入したブレークポイントを取り除きました。

再コンパイルすると、トムはまたあるフレームからリスタートしました。先ほどコードをステップ実行していたところだったので、デバッガはフレームの中をステップ実行し始めました。トムが"0"(ゼロ)キーを押して step-continue という名のリスタートを呼ぶと、デバッガは通常実行に戻りました。このときトムはまだバグに遭遇する前のフレームからリスタートしたので、コードはあたかも最初からバグがなかったかのように走ります!

まとめましょう。コードがエラーを通知すると、トムはデバッガに入っていることに気付きます。トムはコードにブレークポイントを挿入し、バグをつきとめるまでコードの振る舞いを追うことができます。トムがコードを修正したため、彼がフレームをリスタートするとコードは初めからバグがなかったかのように振る舞いました!

実行時にコードを再コンパイルできるのは、Common Lispが備えるたくさんの驚くべき特徴のうちたったひとつにすぎません。次回は、デバッガ内からオブジェクトを調べたり変更するための、Slimeのインスペクタについてお話しします。

原文: http://malisper.me/2015/07/07/debugging-lisp-part-1-recompilation/


  1. 彼はすばらしいプログラマーかもしれませんが、エラーメッセージを読まないのです。

  2. breakはそれ自体がCommon Lispのリスタート(restart)という仕組みで実装されています。リスタートについては第4回で扱います。

Comments