glider-gun's Blog

何か書きます

点字でライフゲーム

roswell スクリプトとしてライフゲームを作ってみました。 前回同様コンソールで動かすのに cl-charms を使っています。 こんな感じです。

動画を見ると点が一番上のステータス表示より高い密度で表示されてるのがわかると思いますが、これはパターンの表示に点字を使ってみているからです。点字は縦横4x2の8点のパターンからなるので、これを使うとライフゲームの8倍高密度な表示が出来ます。unicodeの点字の文字コードと点の配置とは割と規則的に作られているので、盤面から文字コードを逆算するようにしました(参照: https://en.wikipedia.org/wiki/Braille_Patterns )。 行と行の間で多少すきま開いたりはしてますが、そんな悪くないんじゃないでしょうか。

今回これを作っていて、ターミナルとして使うアプリによって動作速度が違うのが興味深かったです。上の動画はmacに元々入っているターミナルで動作させているのですが、iTerm2では(特にフルスクリーン状態では)フレームレートが10倍くらい落ちます。どうもncursesでワイド文字を描画しようとすると大きな速度差があるみたいです(ascii文字の範囲ではどっちで動かしても高速に動作していました)。

処理系はsbclのみ対応です。最新版のcl-charmsでないと動かないので、cl-charms関連でエラーが出たら一旦

1
2
$ ros run
* (ql:update-all-dists)

を試して見るといいかもしれません。あるいは

1
$ ros use sbcl

が必要かも?

Roswellでlessもどきを作ってみる

コンソール上で動くような少しリッチなプログラムを作ろうとする時よく使われるライブラリとしてncursesというのがあります。 それのCommon Lispバインディングとしてはcl-charmsというのがあります。

練習として less コマンドのようなものを作ってみました。roswellスクリプトです。

1
./less.ros hoge.txt

のようにして起動します。

機能としてはhjklか矢印キーで移動してqで終了するだけです。 文字幅を取得するのに cffi で wcwidth 関数を呼んでいます1


  1. このへんはlemの実装の真似をしています

Debugging Lisp を翻訳しました

Common Lispのデバッグに関する Michael Malis 氏の連載 Debugging Lisp の翻訳がひとまず終わりましたので、まとめてリンクを張っておきます。Lisp Advent Calendar 2015の第18日目として第1回の記事へのリンクを張ってましたが、こっちに張り替えます。

つたない翻訳ではありますが、Common Lisp のデバッグ方法についてはあまり日本語で情報がないので、こういうのもあるといいのではないかと思い自分の勉強を兼ねて訳してみた次第です。英語圏でもあんまりみたことない素晴らしい内容ですし1。 翻訳元のサイト http://malisper.me/ には主にマクロについて他にも面白い記事があるので読める方は読むといいと思います。

ちなみにもともとは Qiita のエントリーにするつもりだったんですが、どうも Qiita が gif アニメーション画像に対応していないようだったので急遽 github io を使ってページを作っています。テーマとかまだいじるかもしれません。とりあえずはしれ!コード学園 のLispちゃん会には間にあわせたいという裏目標は達成出来たのでよかったです。


  1. あんまり色々追っているわけでもないですが

Debugging Lisp Part 5: その他こまごま

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

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


このエントリーは、それだけで記事にするほど大きくはないような細かな機能についてです。これまで再コンパイルインスペクトクラスの再定義リスタートを扱いました。

あまり有名でない機能として、 SBCL の trace があります。SBCL のトレース機能はほとんどの Common Lisp 実装のそれに比べはるかに高機能です。SBCL の trace はいくつかのキーワード引数1を(訳注: Common Lispの仕様で決まっている他にも)追加で対応しています。例えば trace はキーワード変数 :break をとります。 :break の値として渡された式はトレース中の関数が呼ばれるたびに評価されます。そしてその式の値が真であるときにデバッガが呼び出されます。例えば次のようなフィボナッチ関数があったとしましょう:

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

trace を使って、 fib が引数ゼロを渡されて呼び出されたときブレークするようにできます:

式の中で関数に渡された引数を参照したいのでちょっとしたひねりが必要になっています。 他にもtrace:break の変種いくつかに対応しています。例えば :break-after はトレースされた関数が呼ばれる前ではなく呼ばれた後に式を評価するものです。 :print:print-after は break に似ていますが、デバッガに入らず単に式の値をプリントします。:print-after を使って、例えば fib が実行から帰るたびに時刻(unix time)をプリントすることができます:

trace の取れる引数の完全なリストを見たければ、 SBCL マニュアルのこのページを見てみてください。

他にあまり知られていない機能は、相互参照(cross reference)コマンド群です。相互参照コマンドは、あるものが参照されているような場所をすべて見つけるコマンドです。これらのコマンドのキーバインディングはすべて C-c C-w で始まっています2。私がもっともよく使う相互参照コマンドは “slime-who-calls” (C-c C-w C-c にバインドされています)で、これは関数が呼ばれている全ての場所を表示します。次の図は scan 関数が cl-ppcre というライブラリの中のどこで呼ばれているか見つけて、その中をスクロールしているところです3:

slime-who-calls は、関数がどのように使われることを想定しているか見るのに便利です。全ての用例を引っ張ってくることができるので、それを眺めるのです。slice-who-call にもいくらかの親戚がいます。 slime-who-macroexpands (C-c C-w RET) はマクロが使われている全ての場所を表示するもので、 slime-who-references (C-c C-w C-r) はそれの変数用バージョンです。

もうひとつ大事な機能として、デバッガの中にいるとき、あるスタックに対応する関数のソースコードを引っ張ってくるものがあります。それを使うひとつの方法は、ソースが見たいスタックにカーソルをもっていって ‘v’ キーを押すことです。もしくは M-p ( Alt キーと ‘p’ キーを同時に押す) と M-n を使ってスタックフレーム中を上下に動くという方法もあります。C-p C-n のかわりにこの2つのコマンドを使うと、 Slime は自動的に対応するソースコードを表示します。次の図は cl-ppcre に正しくない正規表現を渡して、エラーでデバッガに入り、スタックの中を M-n でたどっているところです:

そして IDE のコマンドで一番おなじみの機能、ソースへのジャンプです。最近私が話した人などは、IDE の機能は定義を簡単に探す機能しか使っていないと言っていました。Emacs で Slime を使うと、ほとんど何の定義でも “M-.” (Escキーの後にピリオドキー(訳注: あるいはAlt+ピリオドキー)) で飛ぶことができます。関数、変数、クラス、その他いろいろなもので動作します。総称関数 generic function の定義に飛ぼうとすると、それを実際に定義する全てのメソッド定義が表示されます。例えば create-matcher-aux (これは cl-ppcre ライブラリのほとんどの仕事を行っている関数です) の定義に飛ぼうとすると、図のようになります:

元の場所に戻るには “M-,” を使います。

そしてこれが、 Common Lisp のデバッグであなたが必要になるだろう知識のすべてです。

原文: http://malisper.me/2015/08/19/debugging-lisp-part-5-miscellaneous/


  1. キーワード引数というのは名前がついたオプション引数です。キーワード引数を使うには、その名前と使いたい値を並べて関数に渡します。キーワード引数を使うと、関数は複数のオプション引数を持ったうえでその好きな一部分だけを受け取れるようになります。

  2. C-c は Slime 自体が使っているバインディングです。 C-w は “who(誰が)” の意味で、すべての相互参照コマンドがここに割り当てられています。

  3. マクロの使われている場所の中で、展開形の中でその関数が使われているような場所もすべて引っ張ってきます。

Debugging Lisp Part 4: リスタート

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

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


このエントリーは連載Debbugging Lispの第4回です。これまでに再コンパイルインスペクトクラスの再定義について扱いました。

多くの言語において、エラーを扱う機能は2つに分かれています。いわゆる throw と catch です。 throw は何かが失敗したことを検知し、エラーを何らかの方法で通知します。このとき throw は発生した問題の情報を含んだ例外オブジェクトを作ります。一方 catch はこの例外オブジェクトを受け取って、エラーからの復帰を試みます。

throw / catch の問題は、 throw が catch への無条件の goto のように振る舞うことです。このため、 throw が呼ばれた状況について、例外オブジェクトに含められなかった情報の全ては失われてしまいます。これは catch する側のコードが、エラーからの復帰のためにエラー発生時のため欲しい情報があったとき問題になります。

たとえばあなたは、いくつかのファイルを取ってその中の数のリストをパースするようなライブラリを作っているとしましょう。その最初の関数 read-file は一つのファイルを読んで結果のリストを返します。2つ目の関数 read-files はファイルのリストを取って、各々のファイルの内容をさらにリストにして返します。以下はエラー処理を考えない場合のコードの例です:

1
2
3
4
5
6
7
8
9
(defun read-file (file)
  (with-open-file (in file :direction :input)
    (loop for line = (read-line in nil in)
          until (eq in line)
          collect (parse-integer line))))
 
(defun read-files (files)
  (loop for file in files
        collect (read-file file)))

ライブラリをテストするために、あなたは手元にファイルを 2 つ持っています。1つ目の内容は 5, 10, 15, 20, 25 で、2つ目は 5, 10, 15, 20, a, 30, 40 です。ライブラリがエラーを正しく処理することを確かめるために、2つ目のファイルには “a” とだけ書かれた行を含めることにしたのです。現状では、 parse-integer がこの行でエラーを通知します。ライブラリのテストを簡単にするため、あなたは2つのファイルへのパスネームのリストを変数 *files* に保存しました。このライブラリをこの2つのファイルに対して走らせるとこうなります:

1
2
3
(read-files *files*)
 
 => ERROR

2つ目のファイルの “a” によってエラーが発生しました。あなたはライブラリの設計者として、このような状況で何が起こるべきか、決めなければいけません。使っている言語が catch / throw しか提供しない場合に取れる選択肢を見ていきましょう。

1つ目の選択肢は、単にエラーを起こした項目を無視することです。これを行うためには、Common Lisp での catch 機能にあたる handler-case を使うことができます:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
(defun read-file (file)
  (with-open-file (in file :direction :input)
    (loop for line = (read-line in nil in)
          until (eq in line)
          when (handler-case (parse-integer line)
                 ;; C is the name being used to
                 ;; refer to the exception object.
                 (error (c)
                   (declare (ignore c))
                   nil))
          collect it)))
 
(read-files *files*)
 
=> ((5 10 15 20 25) (5 10 15 20 30 40))

もう一つの選択肢は、ライブラリのユーザーが設定出来るようなスペシャル変数1を提供して、不正な値の代わりに用いられる値を設定出来るようにすることです:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(defvar *malformed-value* nil)
 
(defun read-file (file)
  (with-open-file (in file :direction :input)
    (loop for line = (read-line in nil in)
          until (eq in line)
          when (handler-case (parse-integer line)
                 (error (c)
                   (declare (ignore c))
                   *malformed-value*))
          collect it)))
 
(let ((*malformed-value* :malformed))
  (read-files *files*))
 
=> ((5 10 15 20 25) (5 10 15 20 :MALFORMED 30 40))

3つめの選択肢は、 read-files がエラーを catch して、不正な値の入ったファイル全体をスキップするようにすることです:

1
2
3
4
5
6
7
8
9
10
11
(defun read-files (files)
  (loop for file in files
        when (handler-case (read-file file)
               (error (c)
                 (declare (ignore c))
                 nil))
        collect it))
 
(read-files *files*)
 
=> ((5 10 15 20 25))

最後の選択肢は、ライブラリのユーザー自身に例外処理をまかせることです:

1
2
(handler-case (read-files *files*)
  (error (c) (do-something)))

ユーザーにとっては、最後の選択肢はいくらか有用です。エラーをどう処理するかの柔軟性があるからです。こうすることの問題は、上で述べたように、ユーザにとってはエラーから正しく復帰するようにすることが難しいことです。単にエラーが発生したファイル1つだけをスキップしたい場合、簡単な方法はありません。エラー処理のコードが走るのは関数 read-files を去ったあとだからです。これは残りの読むべきファイルなど、状態に関する全ての情報が catch の時には失われているということを意味します。

catch / throw についてのもう一つの問題は、上で上げた 4 つの選択肢のうち、あなたはたった 1 つしか選ぶことが出来ないということです。どの 1 つもほかの 4 つと競合してしまいます。これもやはり、 throw が goto のようにふるまうからです。一度どこへ飛ぶか決めたら、次にどうするかコントロールすることができません。エラーの処理をライブラリのユーザー自身にまかせることにしたなら、状態に関する情報が全て失われるために、ユーザーには簡単にエラーをうまく処理する方法がありません。

ここでリスタートが登場します。Common Lisp では、 catch は2つの異なる部分に分かれています: ハンドラー handler とリスタート restart です。ハンドラーはライブラリのユーザーが束縛し、例外が発生したときに何が起こるべきかを決めるためのものです。リスタートはエラーから復帰する選択肢を提供するため、ライブラリ側で定義します。 もしリスタートに対応した言語を使っていれば、先ほどの選択肢の最初の3つをリスタートとして実装することが出来ます。そしてユーザーがライブラリを使うときには、エラーの処理にそのうちどれを使いたいかを選ぶことが出来ます。以下が先ほどのライブラリを、先ほどの選択肢にそれぞれ対応する3つのリスタートが使えるよう再実装したものです:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
(defun ask (string)
  (princ string *query-io*)
  (read *query-io*))
 
(defun read-file (file)
  (with-open-file (in file :direction :input)
    (loop for line = (read-line in nil in)
          until (eq in line)
          when (restart-case (parse-integer line)
                 (use-value (value)
                   :report "Use a new value."
                   :interactive (lambda ()
                                  (list (ask "Value: ")))
                   value)
                 (skip-entry ()
                   :report "Skip the entry."
                   nil))
          collect it)))
 
(defun read-files (files)
  (loop for file in files
        when (restart-case (read-file file)
               (skip-file ()
                 :report "Skip the entire file."
                 nil))
        collect it))
 
;;; The three functions below are predefined
;;; handlers for the most common ways the user
;;; will interact with the restarts.
(defun skip-entry (c)
  (declare (ignore c))
  (invoke-restart 'skip-entry))
 
(defun skip-file  (c)
  (declare (ignore c))
  (invoke-restart 'skip-file))
 
(defun use-value-handler (value)
  (lambda (c)
    (declare (ignore c))
    (invoke-restart 'use-value value)))

リスタートはマクロ restart-case で定義され、関数 invoke-restart によって呼び出されます。いくらか簡略化して言うと、リスタートを呼び出すとのは、エラーが通知されたところからリスタートの本体(body)にジャンプするのと実際上同じことです。これは、リスタートが確立された時点でスタックに保存されていた全ての状態が、リスタートが呼び出された時点でまだ使えるということを意味します。これにより、ライブラリのユーザーに、エラー発生時にどうするかについてずっと粒度の細かいコントロールを与えることができます。

エラー時に何が起こるべきかユーザーが決めるには、マクロ handler-bind を使うだけです。 handler-bind はエラーの種類と、その種類のエラーが通知されたときに呼び出すべきハンドラー(これは関数のはずです)を引数に取ります。ハンドラーはそのとき invoke-restart を呼ぶことで、ライブラリが持っているリスタートのうち一つを呼び出します。上のライブラリのコードには、リスタートひとつにつきひとつのハンドラー関数が用意してありますが、これはこれらが最もよくある種類のハンドラーだからです。以下はそれぞれのハンドラーが先ほどの2つのファイルに対して使われたときの例です:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(handler-bind ((error #'skip-entry))
  (read-files files*))
 
=> ((5 10 15 20 25) (5 10 15 20 30 40))
 
(handler-bind ((error #'skip-file))
  (read-files files*))
 
=> ((5 10 15 20 25))
 
(handler-bind ((error (use-value-handler 0)))
  (read-files files*))
 
=> ((5 10 15 20 25) (5 10 15 20 0 30 40))

リスタートが本当にクールなのは、ユーザーがハンドラーを設定していなかったときの振る舞いです。この場合ユーザーはSlimeデバッガに入ることになります。可能なリスタートのリストが提示されて、まるで最初からエラーがハンドリングされていたかのようにエラーを処理することができます!以下はユーザーがエラーをハンドルせず、そして実行中に (デバッガから) skip-entry リスタートを選んだ時の様子です:

さらにクールなこととして、この"対話的リスタート"はブレークポイントを実装するのに使えます!第1回で書いたように、 Common Lisp はブレークポイントをエディタの機能としてではなく関数 “break” として提供しています。例えば次のような実装ができるでしょう:

(defun break (&optional (format-control "Break")
              &rest format-arguments)
   (with-simple-restart (continue "Return from BREAK.")
     (let ((*debugger-hook* nil))
       (invoke-debugger
         (make-condition 'simple-condition
           :format-control   format-control
           :format-arguments format-arguments))))
   nil)

このコードは “continue” というリスタートを用意してからエラーを通知することで動作します。つまり、breakが呼ばれた直後、実行を続行するという選択肢つきのデバッガに入るわけです。この動作はブレークポイントそのものです。

リスタートは Common Lisp のデバッギングにおける素晴らしい機能のひとつです。エラーが発生したとき、よりよい制御を可能にしてくれます。しかも、自分でそれをハンドルしなかったときには対話的にリスタートを選んでコードの実行を続けられるのです。

原文: http://malisper.me/2015/08/05/debugging-lisp-part-4-restarts/


  1. ダイナミック変数とは、要するにシャドウ(shadow、覆い隠す)可能なグローバル変数です。ダイナミック変数がシャドウされると、変数に対するすべての参照は新しい値を指すようになります。変数をシャドウしたフォームから抜けた時点で、ダイナミック変数は以前の値に戻ります。

Debugging Lisp Part 3: クラスの再定義

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

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


このエントリーは連載 Debbugging Lisp の第3回です。第1回はこちら、第2回はこちら

Common Lisp Object System (CLOS) は非常に強力です。多重継承、多重ディスパッチ、メソッドの振る舞いを拡張するさまざまな異なるやり方を提供します。その内部では、ほとんどの実装は Metaobject Protocol(MOP) を使っています。これは CLOS 自身を使って CLOS を定義するものです。 MOP では、クラスはいくつかのインスタンス変数を持ったオブジェクトとして実装されます。そのインスタンス変数としては、クラス名、スーパークラス、またそのクラスの持つインスタンス変数のリストなどがあります。前回のエントリーの point クラスを見てみましょう:

1
2
3
(defclass point ()
  ((x :accessor point-x :initarg :x :initform 0)
   (y :accessor point-y :initarg :y :initform 0)))

そして Slime インスペクタを使って point クラスオブジェクトを見てみましょう。クラスオブジェクトは関数 find-class で見つけることができます:

MOP の利点は、 CLOS の振る舞いを微調整したいときには普通のオブジェクト指向プログラミングによって行えるということです。優れた例として、任意の述語(predecated)を CLOS のディスパッチに使えるようにするfilterd-functionsライブラリがあります。MOPについてはもういいでしょう1。このエントリーでは CLOS の小さな一部分である、 update-instance-for-redefined-class についてお話しします。

update-instance-for-redefined-class は、クラスが(実行時に)再定義される度に呼ばれるメソッドです。これをオーバーライドすることでクラス再定義の際の振る舞いをカスタマイズすることが出来ます。 たとえば何かのシミュレーション中で、先ほどのpointクラスを複素数を表すのに使っていたとしましょう。シミュレーションの一部として、pointオブジェクトを変数 *location* に保存しています。

シミュレーションをプロファイリングしてみると、ボトルネックの一つが複素数同士のかけ算であることに気付きました。複素数のかけ算は極座標表示での方がずっと効率的ですから、あなたは point クラスの実装を直交座標から極座標に変えることにしました。これを(実行中に)行うためには、次のコードを走らせるだけです:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(defmethod update-instance-for-redefined-class :before
     ((pos point) added deleted plist &key)
  (let ((x (getf plist 'x))
        (y (getf plist 'y)))
    (setf (point-rho pos) (sqrt (+ (* x x) (* y y)))
          (point-theta pos) (atan y x))))
 
(defclass point ()
  ((rho :initform 0 :accessor point-rho)
   (theta :initform 0 :accessor point-theta)))
 
(defmethod point-x ((pos point))
  (with-slots (rho theta) pos (* rho (cos theta))))
 
(defmethod point-y ((pos point))
  (with-slots (rho theta) pos (* rho (sin theta))))

これは要するに、 update-instance-for-redefined-class を拡張して、極座標実装での値 rhotheta を直交座標実装の値から計算するものです。 update-instance-for-redefined-class を拡張した後、このコードはクラスを再定義して、その時存在する全てのインスタンスを新しいクラスのインスタンスに変換します2。 そして最後に、二つのメソッド point-xpoint-y を定義し、 point クラスの元のインターフェースを保ちます3。コードを走らせてから *location* の中身をインスペクトすると、次のようなものが見えるはずです:

*location* は依然として同一のオブジェクトを指しているものの、今やそれは極座標実装になっています! 正しく変換されたことを確かめるため、あなたはこのオブジェクトを与えて point-x 関数を呼び出し、 x 座標が同じままかどうか確かめることにしました:

驚くべきことに、クラスの実装を完全に置き換えたにもかかわらず、全てのコードは動作し続けます。もしあなたが24時間365日動き続けていなければならないサービス内でクラスの実装を変更したくて、しかもそれがたまたま Common Lisp で書かれていたなら、 update-instance を使うことを思い出してください。

原文: http://malisper.me/2015/07/22/debugging-lisp-part-3-redefining-classes/


  1. MOPについてもっと学びたい方は、"The Art of the Metaobject Protocol"を手に取ってみてください。オブジェクト指向プログラミングを作った人であるアラン・ケイは、1997年のOOPSLAでの講演でこの本を「この10年で最も良い本」だと言いました。

  2. 実際には、どの時点でupdate-instance-for-redefined-classが呼ばれるかは仕様では策定されていません。 保証されているのは、最初にインスタンス変数がアクセスされるまでに呼ばれるということだけです。

  3. 本当にインターフェースを保存するためには initialize-instance などいくつか他の関数も定義し直す必要はあります。

Debugging Lisp Part 2: インスペクト

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

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


このエントリーは連載 Debugging Lispの第2回です。第1回はこちら

このエントリーではCommon Lispのもう一つのデバッグツール、Slimeのインスペクタについて議論します。 Slimeインスペクタは、オブジェクトをreplから直接操作することを可能にします。 色々なことができ、例えばオブジェクトをクリックして内容をみることもできますし、オブジェクトをコピーアンドペーストして後の関数呼び出しで使うこともできます1。 次のようなpointクラスがあるとしましょう:

1
2
3
(defclass point ()
  ((x :accessor point-x :initarg :x :initform 0)
   (y :accessor point-y :initarg :y :initform 0)))

このクラスのインスタンスを作るには次のようにします:

1
(make-instance 'point :x 10 :y 20)

返されたオブジェクトを右クリックして"inspect"オプションを選ぶか、emacsのショートカット"C-v C-v TAB"を使うことでオブジェクトの中をのぞくことができます:

全てのインスタンス変数の現在の値が表示されます。表示するだけでなく、変数の値を変更することも出来ます。これらがデバッガ内から、実行時に使えることが強力なのだということに注意してください。

実際にオブジェクトの値が変更されたことを確認するには、このオブジェクトをコピーアンドペーストし、 point-x 関数に与えて呼び出すことができます。

インスペクタに関してもう一つとてもクールなのは、Slimeのトレースダイアログです。トレースダイアログは普通のトレースに似ていますが、トレースされている関数が渡されたり返すオブジェクトがインスペクトできるようになっています。例えば、あなたが次のような末尾再帰最適化された関数 sum を書いているとしましょう。これはリストの中の全ての数を足し合わせます。

1
2
3
4
5
6
7
(defun sum (xs &optional (acc 0))
  (if (null xs)
      acc
      (sum (cdr xs) (+ (car xs) acc))))
 
(sum '(1 2 3))
=> 6

関数 sum をトレースするかどうかは、ショートカット “C-c M-t” の後、関数の名前 “sum” を打つことで切り替えることができます。関数をトレースした後、"C-c T"でインタラクティブなトレースダイアログのバッファに入れます。"G"を押すことで情報を更新して一番最後のトレースを得ることができます。

このトレースは普通のトレースと大体同じ見た目をしていますが、よい機能があります。上で述べたように、関数の全ての引数と返り値をインスペクトすることができるのです。見やすいようにトレースの枝の表示/非表示を切り替えることも出来ます。

Slimeのトレースダイアログは、コードが多数のオブジェクトをやりとりしていて、自分がその中身について確信が持てないときに非常に有用です。これを使うだけであなたに必要な情報の全てを追ってくれるのです。

SlimeのインスペクタはCommon Lispのデバッグツール群のもう一つの素晴らしい機能です。プログラムがクラッシュし、原因が分からないときに重宝します。他のデバッグツール群と組み合わされば最高です。

原文: http://malisper.me/2015/07/14/debugging-lisp-part-2-inspecting/


  1. 技術的にはこれはインスペクタの機能ではなく"Presentation"の機能です。インスペクタはオブジェクトの中を見る機能にあたります。

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回で扱います。