The Common Lisp Cookbook – エラーと例外処理

Table of Contents

The Common Lisp Cookbook – エラーと例外処理

📢 🎓 ⭐ Learn Common Lisp efficiently in videos, by the Cookbook's main contributor. Learn more.

🖊️ Discover a new Common Lisp and Coalton editor for beginners: mine and a new VSCode extension for Common Lisp: OLIVE.

Common Lisp には、ほかの言語に見られるようなエラー処理と condition 処理の仕組みがあり、さらにそれ以上のことができます。

condition とは何でしょうか?

例外処理をサポートする言語(Java、C++、Python など)と同じく、condition は多くの場合「例外的」な状況を表します。しかし、それらの言語以上に、Common Lisp の condition は、必ずしもエラー状態に起因しない、プログラムロジック上の分岐が必要になる一般的な状況を表すことができます。Lisp 開発の非常に対話的な性質(REPL と組み合わせた Lisp イメージ)を考えると、これは Java や、非常に原始的な REPL しか持たない Python のような言語よりも、Lisp のような言語では完全に理にかなっています。ただし多くの場合、このシステムが提供する対話性を必要としない(あるいは許可すらしない)かもしれません。ありがたいことに、同じシステムは非対話モードでも同じようにうまく機能します。

z0ltan

順を追って見ていきましょう。追加の資料は後半にあります。

Throwing/catching と signaling/handling

Common Lisp には throw と catch の概念がありますが、これは C++ や Java の throwing/catching とは異なる概念を指します。Common Lisp の throwcatchRuby と同じく!)は制御の移動のための仕組みであり、condition を扱うものではありません。

Common Lisp では condition は signaled され、signaled された condition に応じてコードを実行する過程を handling と呼びます。Java や C++ と違い、condition を handle することは、ただちにスタックが巻き戻されることを意味しません。スタックを巻き戻すかどうか、どの状況でそうするかは、個々の handler 関数が決めます。

すべてのエラーを無視し、nil を返す

関数が失敗し得ることを知っていて、それをただ無視したい場合があります。そのときは ignore-errors を使います。

(ignore-errors
  (/ 3 0))
; in: IGNORE-ERRORS (/ 3 0)
;     (/ 3 0)
;
; caught STYLE-WARNING:
;   Lisp error during constant folding:
;   arithmetic error DIVISION-BY-ZERO signalled
;   Operation was (/ 3 0).
;
; compilation unit finished
;   caught 1 STYLE-WARNING condition
NIL
#<DIVISION-BY-ZERO {1008FF5F13}>

ありがたいことに division-by-zero の警告は得られますが、コードは実行され、2 つの値を返します。nil と、signal された condition です。何を返すかは選べませんでした。

Slime では右クリックで condition を inspect できることを思い出してください。

handler-case ですべての error condition を扱う

ignore-errorshandler-case から作られています。前の例を、一般的な error を handle する形で書けますが、今度は好きなものを返せます。

(handler-case (/ 3 0)
  (error (c)
    (format t "We handled an error.~&")
    (values 0 c)))
; in: HANDLER-CASE (/ 3 0)
;     (/ 3 0)
;
; caught STYLE-WARNING:
;   Lisp error during constant folding:
;   Condition DIVISION-BY-ZERO was signalled.
;
; compilation unit finished
;   caught 1 STYLE-WARNING condition
We handled an error.
0
#<DIVISION-BY-ZERO {1004846AE3}>

ここでも 2 つの値、0 と signal された condition を返しました。

handler-case の一般形は次のとおりです。

(handler-case (code that can error out)
   (condition-type (the-condition) ;; <-- optional argument
      (code))
   (another-condition (the-condition)
       ...))

handler-case に続く (code that can error out) は 1 つのフォームでなければなりません。複数の式を書きたい場合は progn で包みます。

特定の condition を扱う

どの condition を扱うか指定できます。

(handler-case (/ 3 0)
  (division-by-zero (c)
    (format t "Got division by zero: ~a~%" c)))
;; …
;; Got division by zero: arithmetic error DIVISION-BY-ZERO signalled
;; Operation was (/ 3 0).
;; NIL

これは、ほかの言語で知られる「通常の」例外処理に最も近い仕組みです。C++ と Java の throw/try/catch、Python の raise/try/except、Ruby の raise/begin/rescue などです。しかし、Common Lisp ではもっと多くのことができます。

condition と restart を完全に制御する: handler-bind

handler-bind は、condition が signal されたときに何が起きるかを完全に制御したい場合に使います。スタックを巻き戻しません。この点は次の節で示します。デバッガと restarts を、対話的にもプログラム的にも使えるようにします。

一般形は次のとおりです。

(handler-bind ((a-condition #'function-to-handle-it)
               (another-one #'another-function))
    (code that can...)
    (...error out…)
    (... with an implicit PROGN))

例:

(defun handler-bind-example ()
  (handler-bind
        ((error (lambda (c)
                  (format t "we handle this condition: ~a" c)
                  ;; この return-from を外して試すと、エラーは対話的デバッガまで伝播する。
                  (return-from handler-bind-example))))
      (format t "starting example…~&")
      (error "oh no")))

handler-case と比べると構文が「逆」になっていることに気づくでしょう。先に bindings があり、次に(暗黙の progn 内の)フォームがあります。

handler が通常どおり戻った場合(condition の処理を辞退した場合)、condition は別の handler を探して伝播し続け、最終的に対話的デバッガに到達します。

これも handler-case との違いです。handler 関数が return-from handler-bind-example で呼び出し元関数から明示的に戻らなければ、エラーは伝播し続け、対話的デバッガが表示されます。

この挙動は、プログラムが simple condition を signal したときに特に便利です。simple condition はエラーではないため(下の「condition 階層」を参照)、デバッガを起動しません。condition(アプリケーション内で何かが起きたという signal)に対して何か処理を行い、プログラムを続行できます。

あるライブラリがすべての condition を処理せず、いくつかをこちらへ伝播させる場合、スタックの深い場所にある restarts(restart-case により確立されたもの)を見ることができます。そのライブラリが呼び出した別ライブラリが確立した restarts も含まれます。

handler-bind はスタックを巻き戻さない

handler-bind では、すべての呼び出しフレームを含む完全なスタックトレースを見られますhandler-case を使うと、condition が処理されるまでのプログラム実行の多くの段階を「忘れ」ます。コールスタックが巻き戻されるためです。handler-bind はスタックを巻き戻しません。これを示します。

デモのため、Quicklisp でインストールできる trivial-backtrace ライブラリを使います。

(ql:quickload "trivial-backtrace")

これは sb-debug:print-backtrace など、処理系のプリミティブを包むラッパーです。

次のコードを考えます。main 関数は、最終的に error を signal して失敗する関数の連鎖を呼び出します。main 関数でエラーを処理し、バックトレースを出力します。

(defun f0 ()
  (error "oh no"))

(defun f1 ()
  (f0))

(defun f2 ()
  (f1))

(defun main ()
  (handler-case (f2)
    (error (c)
      (format t "in main, we handle: ~a" c)
      (trivial-backtrace:print-backtrace c))))

これがバックトレースです(最初のフレームだけ)。

CL-REPL> (main)
in main, we handle: oh no
Date/time: 2025-07-04-11:25!
An unhandled error condition has been signalled: oh no

Backtrace for: #<SB-THREAD:THREAD "repl-thread" RUNNING {1008695453}>
0: […]
1: (TRIVIAL-BACKTRACE:PRINT-BACKTRACE … )
2: (MAIN)
[…]

ここまでは問題ありません。”Date/time” と “An unhandled error condition…” というメッセージを出しているのは trivial-backtrace です。

次に、handler-bind を使った場合の stacktrace と比較します。

(defun main-no-stack-unwinding ()
  (handler-bind
      ((error (lambda (c)
                (format t "in main, we handle: ~a" c)
                (trivial-backtrace:print-backtrace c)
                (return-from main-no-stack-unwinding))))
    (f2)))
CL-REPL> (main-no-stack-unwinding)
in main, we handle: oh no
Date/time: 2025-07-04-11:32!
An unhandled error condition has been signalled: oh no


Backtrace for: #<SB-THREAD:THREAD "repl-thread" RUNNING {1008695453}>
0: …
1: (TRIVIAL-BACKTRACE:PRINT-BACKTRACE …)
2: …
3: …
4: (ERROR "oh no")
5: (F0)
6: (F1)
7: (MAIN-NO-STACK-UNWINDING)

その通りです。main 関数から f1f0 を通ってエラーに至る、すべてのコールスタックが見えます。handler-case を使ったときのバックトレースに、この 2 つの中間関数はありませんでした。エラーが signal され、コールスタックを伝播するにつれて、スタックが unwound(「ほどかれた」「短くされた」)され、情報を失ったからです。

どちらをいつ使うか

失敗する状況を予期しているなら、handler-case で十分です。たとえば HTTP リクエストの文脈では、400 番台のエラーを想定するのは普通です。

;; dexador ライブラリを使う。
(handler-case (dex:get "http://bad-url.lisp")
  (dex:http-request-failed (e)
    ;; 4xx または 5xx HTTP エラー: 起こり得るので問題ない。
    (format *error-output* "The server returned ~D" (dex:response-status e))))

その他の例外的な状況では、おそらく handler-bind が欲しくなります。たとえば、何が問題だったかを処理し、バックトレースを出力したい場合、または手動でデバッガを起動し(下記参照)、正確に何が起きたかを見たい場合です。

condition の有無にかかわらずコードを実行する(”finally”)(unwind-protect)

ほかの try/catch/finally フォームにおける “finally” 部分は unwind-protect で行います。

これは with-open-file のような “with-“ マクロで使われる構文で、処理後に必ずファイルを閉じます。

この例では:

(unwind-protect (/ 3 0)
  (format t "This place is safe.~&"))

SBCL ソース:

(sb-xc:defmacro with-open-file ((stream filespec &rest options)
                                &body body)
  (multiple-value-bind (forms decls) (parse-body body nil)
    (let ((abortp (sb-xc:gensym)))
      `(let ((,stream (open ,filespec ,@options))
             (,abortp t))
         ,@decls
         (unwind-protect
              (multiple-value-prog1
                  (progn ,@forms)
                (setq ,abortp nil))
           (when ,stream
             (close ,stream :abort ,abortp)))))))

単純化すると:

(defmacro with-open-file ((stream filespec) &body body)
  `(let ((,stream (open ,filespec)))
    (unwind-protect
      (progn ,@body)
     (when ,stream
       (close ,stream)))))

なぜなら、次を:

(let ((stream (open "filename.txt" :direction :input :if-does-not-exist :create :if-exists :overwrite)))
    (unwind-protect
      (format stream "hello file")
     (when stream
       (close stream))))

次のように単純に書きたいからです。

(with-open-file (f "filename.txt" …)
  (format stream "hello"))

対話的デバッガは表示されますhandler-bind などは使っていません)が、それでもその後にメッセージは出力されます。

condition を定義し作る

define-condition で condition を定義し、make-condition でそれを作成(初期化)します。

(define-condition my-division-by-zero (error)
  ())

(make-condition 'my-division-by-zero)
;; #<MY-DIVISION-BY-ZERO {1005A5FE43}>

condition を作るときにより多くの情報を与えた方がよいので、slots を使いましょう。

(define-condition my-division-by-zero (error)
  ((dividend :initarg :dividend
             :initform nil
             :reader dividend)) ;; <-- (dividend condition) で dividend を得られる。必要なら CLOS チュートリアルを参照。
  (:documentation "Custom error when we encounter a division by zero.")) ;; よい習慣 ;)

これで、コード内で condition を signal するときに、後で利用される情報を埋め込めます。

(make-condition 'my-division-by-zero :dividend 3)
;; #<MY-DIVISION-BY-ZERO {1005C18653}>

Note: Common Lisp Object System についてまだ十分でない場合の、クラスに関する簡単なおさらいです。

(make-condition 'my-division-by-zero :dividend 3)
;;                                   ^^ これが ":initarg"

そして :reader dividend は、my-division-by-zero オブジェクトの dividend に対する “getter” である総称関数を作りました。

(make-condition 'my-division-by-zero :dividend 3)
;; #<MY-DIVISION-BY-ZERO {1005C18653}>
(dividend *)
;; 3

:accessor なら getter と setter の両方になります。

つまり、define-condition の一般形は通常のクラス定義のように見え、感じられます。ただし似ていても、condition は standard object ではありません。

違いの 1 つは、slots に対して slot-value を使えないことです。

condition を signal する: error, cerror, warn, signal

error は 2 通りに使えます。

どちらの場合も、condition が handle されなければ、error は対話的デバッガを開き、ユーザーは実行を続けるための restart を選べます。

上で定義した独自 condition 型では、次のようにできます。

(error 'my-division-by-zero :dividend 3)
;; これは次のショートカット
(error (make-condition 'my-division-by-zero :dividend 3))

cerrorerror に似ていますが、ユーザーが実行を続けるために使える continue restart を自動的に確立します。最初の引数として文字列を受け取り、この文字列はその restart のユーザーに見える report として使われます。

warn はデバッガには入りません(warning condition は [warning][warning] を subclass して作ります)。condition が handle されなければ、代わりに警告を error output へ記録します。

何も出力したくなく、デバッガにも入りたくないが、何らかの注目すべき状況が起きたことを上位レベルへ signal したい場合は、signal を使います。

その状況は、コードの通常動作中に情報を渡すことから、エラーのような重大な状況まで何でも構いません。たとえば、操作中の進捗を追跡するために使えます。percent slot を持つ condition を作り、進捗があるたびに signal し、上位コードがそれを handle してユーザーに表示できます。詳しくは下の資料を参照してください。

condition 階層

simple-error の class precedence list は simple-error, simple-condition, error, serious-condition, condition, t です。

simple-warning の class precedence list は simple-warning, simple-condition, warning, condition, t です。

カスタムエラーメッセージ(:report)

ここまで、エラーを signal したとき、デバッガには次のデフォルトテキストが表示されていました。

Condition COMMON-LISP-USER::MY-DIVISION-BY-ZERO was signalled.
   [Condition of type MY-DIVISION-BY-ZERO]

condition 宣言に :report 関数を与えることで、もっとよくできます。

(define-condition my-division-by-zero (error)
  ((dividend :initarg :dividend
             :initform nil
             :accessor dividend))
  ;; :report はデバッガ内のメッセージ:
  (:report (lambda (condition stream)
     (format stream
             "You were going to divide ~a by zero.~&"
             (dividend condition)))))

すると:

(error 'my-division-by-zero :dividend 3)
;; Debugger:
;;
;; You were going to divide 3 by zero.
;;    [Condition of type MY-DIVISION-BY-ZERO]

stacktrace を調べる

これはもう 1 つの簡単なおさらいであり、Slime チュートリアルではありません。デバッガでは、stacktrace、関数呼び出しへの引数、エラーのあるソース行への移動(Slime では v)、そのコンテキストでのコード実行(e)などができます。

多くの場合、バグのある関数を編集し、(Slime の C-c C-c ショートカットで)コンパイルし、”RETRY” restart を選んで、コードが通ることを確認できます。

これはすべてコンパイラオプション、つまりデバッグ、速度、安全性のどれ向けに最適化されているかに依存します。

デバッグセクションを参照してください。

Restarts、デバッガ内の対話的な選択肢

Restarts はデバッガ内で得られる選択肢です。デバッガには常に RETRYABORT があります。

restart を handling することで、エラーが起きなかったかのように(スタック上で見たように)操作をやり直せます。

assert の任意 restart を使う

単純な形では、assert は私たちが知っているとおりに動きます。

(assert (realp 3))
;; NIL = passed

アサーションが失敗すると、デバッガに入るよう促されます。

(defun divide (x y)
  (assert (not (zerop y)))
  (/ x y))

(divide 3 0)
;; The assertion (NOT #1=(ZEROP Y)) failed with #1# = T.
;;    [Condition of type SIMPLE-ERROR]
;;
;; Restarts:
;;  0: [CONTINUE] Retry assertion.
;;  1: [RETRY] Retry SLIME REPL evaluation request.
;;  …

値を変更する選択肢を提供する任意パラメータも受け取ります。

(defun divide (x y)
  (assert (not (zerop y))
          (y)   ;; 変更できる値のリスト。
          "Y can not be zero. Please change it") ;; カスタムエラーメッセージ。
  (/ x y))

これで、Y の値を変更する新しい restart が得られます。

(divide 3 0)
;; Y can not be zero. Please change it
;;    [Condition of type SIMPLE-ERROR]
;;
;; Restarts:
;;  0: [CONTINUE] Retry assertion with new value for Y.  <--- new restart
;;  1: [RETRY] Retry SLIME REPL evaluation request.
;;  …

それを選ぶと、REPL で新しい値の入力を求められます。

The old value of Y is 0.
Do you want to supply a new value?  (y or n) y

Type a form to be evaluated:
2
3/2  ;; and our result.

restarts を定義する(restart-case)

これは便利ですが、もっと独自の選択肢が欲しいこともあります。restart-case で関数呼び出しを包むことで、一覧の先頭に restarts を追加できます。

(defun divide-with-restarts (x y)
  (restart-case (/ x y)
    (return-zero ()  ;; <-- "RETURN-ZERO" という新しい restart を作る
      0)
    (divide-by-one ()
      (/ x 1))))
(divide-with-restarts 3 0)

任意のエラーの場合(これは handler-bind で改善します)、デバッガの先頭にこの 2 つの新しい選択肢が表示されます。

これで問題ありませんが、より人間に分かりやすい “reports” を書きましょう。

(defun divide-with-restarts (x y)
  (restart-case (/ x y)
    (return-zero ()
      :report "Return 0"  ;; <-- 追加
      0)
    (divide-by-one ()
      :report "Divide by 1"
      (/ x 1))))
(divide-with-restarts 3 0)
;; Nicer restarts:
;;  0: [RETURN-ZERO] Return 0
;;  1: [DIVIDE-BY-ONE] Divide by 1

こちらの方がよいですが、上の assert の例で行ったように operand を変更する機能がありません。

restart で変数を変更する

定義した 2 つの restarts は、新しい値を求めませんでした。これを行うには、restart に :interactive lambda 関数を追加し、ユーザーに新しい値を入力方法で尋ねます。ここでは通常の read を使います。

(defun divide-with-restarts (x y)
  (restart-case (/ x y)
    (return-zero ()
      :report "Return 0"
      0)
    (divide-by-one ()
      :report "Divide by 1"
      (/ x 1))
    (set-new-divisor (value)
      :report "Enter a new divisor"
      ;;
      ;; ユーザーに新しい値を尋ねる:
      :interactive (lambda () (prompt-new-value "Please enter a new divisor: "))
      ;;
      ;; 新しい値で divide 関数を呼ぶ…
      ;; …おそらく不正入力を再び handle しながら!
      (divide-with-restarts x value))))

(defun prompt-new-value (prompt)
  (format *query-io* prompt) ;; *query-io*: ユーザー問い合わせ用の特別なストリーム。
  (force-output *query-io*)  ;; ユーザーが入力内容を見られるようにする。
  (list (read *query-io*)))  ;; リストを返さなければならない。

(divide-with-restarts 3 0)

呼び出すと新しい restart が提示され、新しい値を入力し、結果を得ます。

(divide-with-restarts 3 0)
;; Debugger:
;;
;; 2: [SET-NEW-DIVISOR] Enter a new divisor
;;
;; Please enter a new divisor: 10
;;
;; 3/10

グラフィカルユーザーインターフェイスの方がよいですか? GNU/Linux では zenity コマンドラインインターフェイスを使えます。

(defun prompt-new-value (prompt)
  (list
   (let ((input
          ;; プログラムの出力を文字列へ捕捉する。
          (with-output-to-string (s)
            (let* ((*standard-output* s))
              (uiop:run-program `("zenity"
                                  "--forms"
                                  ,(format nil "--add-entry=~a" prompt))
                                :output s)))))
     ;; 文字列を得たので、数値が欲しい。
     ;; parse-integer や parse-number ライブラリなども使える。
     (read-from-string input))))

もう一度試すと、新しい数値を尋ねる小さなウィンドウが出るはずです。

これは楽しいですが、それだけではありません。restarts を手動で選ぶことは、常に(あるいは頻繁に)満足できるものではありません。そして restart を handling することで、エラーが起きなかったかのように、スタック上で見た操作をやり直せます。

restarts をプログラムから呼び出す(handler-bind, invoke-restart)

condition を signal し得るコードがあるとします。ここでは divide-with-restarts がゼロ除算に関するエラーを signal する可能性があります。やりたいのは、上位レベルのコードがそれを自動的に handle し、適切な restart を呼ぶことです。

これは handler-bindinvoke-restart でできます。

(defun divide-and-handle-error (x y)
  (handler-bind
      ((division-by-zero (lambda (c)
                           (format t "Got error: ~a~%" c) ;; エラーメッセージ
                           (format t "and will divide by 1~&")
                           (invoke-restart 'divide-by-one))))
    (divide-with-restarts x y)))

(divide-and-handle-error 3 0)
;; Got error: arithmetic error DIVISION-BY-ZERO signalled
;; Operation was (/ 3 0).
;; and will divide by 1
;; 3

ほかの restarts を使う(find-restart)

find-restart を使います。

find-restart 'name-of-restart は、指定された名前で最も新しく束縛された restart、または nil を返します。

restarts を隠す、表示する

Restarts は隠せます。restart-case では、:report:interactive に加えて、:test キーも受け取ります。

(restart-case
   (return-zero ()
     :test (lambda ()
             (some-test))
    ...

デバッガを手動で起動する

handler-bind で condition を handle しており、condition オブジェクトが(上の例のように)c 変数に束縛されているとします。さらに、たとえば *devel-mode* というパラメータが、本番環境ではないことを示しているとします。この condition に対してデバッガを起動すると便利かもしれません。次を使います。

(invoke-debugger c)

本番環境では、代わりに trivial-backtrace:print-backtrace でバックトレースを出力できます。

デバッガを無効にする

本番環境では、デバッガをオフにして lisp プログラムを実行できます。各処理系にはコマンドラインスイッチがあります。SBCL では次のとおりです。

$ sbcl --disable-debugger

(これは --script--non-interactive では暗黙に指定されます)。

まとめ

これで、本番コードを書く準備ができました!

資料

関連項目

謝辞

Page source: ja/error_handling.md

T
O
C