0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Common Lisp ベーシックマスター Level 13 (エラー処理)

0
Last updated at Posted at 2025-12-12

1. エラー処理の重要性

1-1. なぜエラー処理が必要か

プログラムは予期しない状況に遭遇する。ファイルが存在しない、ネットワークが切断される、不正な入力を受け取るなど。エラー処理を適切に行わないと、プログラムは突然クラッシュしてしまう。

Common Lisp は コンディションシステム という強力なエラー処理機構を持つ。これは単なる例外処理を超え、エラーからの 復帰 も可能にする。

1-2. コンディションシステムの構成

2. 基本的なエラー

2-1. エラーの発生

;; ゼロ除算
(/ 1 0)
;; → エラー: division by zero

;; 存在しない関数
(undefined-function)
;; → エラー: undefined function

;; 型エラー
(+ "a" 1)
;; → エラー: "a" is not a number

2-2. error 関数

error 関数で意図的にエラーを発生させる。

;; 単純なエラー
(error "Something went wrong")

;; フォーマット付きエラー
(defun divide (a b)
  (if (zerop b)
      (error "Cannot divide ~A by zero" a)  ; エラーメッセージを生成
      (/ a b)))

(divide 10 0)
;; → エラー: Cannot divide 10 by zero

2-3. warn 関数

warn は警告を出すが、プログラムは続行する。

(defun check-value (x)
  (when (< x 0)
    (warn "Negative value: ~A" x))  ; 警告を出力
  (abs x))                          ; 処理は続行

(check-value -5)
;; 警告: Negative value: -5
;; → 5

3. handler-case によるエラー捕捉

3-1. 基本的な使い方

handler-case はエラーを捕捉し、適切に処理する。

(handler-case
    (/ 10 0)                        ; エラーが発生する可能性のある式
  (division-by-zero ()              ; 捕捉するコンディション
    (format t "ゼロで割れません~%")
    nil))                           ; 代わりに返す値
;; ゼロで割れません
;; → NIL

3-2. 構文

(handler-case 
  (コンディション型1 (変数)
    処理1...)
  (コンディション型2 (変数)
    処理2...)
  ...)

3-3. 複数のコンディションを捕捉

(defun safe-divide (a b)
  (handler-case
      (/ a b)
    (division-by-zero ()            ; ゼロ除算
      (format t "ゼロ除算エラー~%")
      nil)
    (type-error (e)                 ; 型エラー
      (format t "型エラー: ~A~%" e)
      nil)))

(safe-divide 10 0)
;; ゼロ除算エラー
;; → NIL

(safe-divide "a" 2)
;; 型エラー: ...
;; → NIL

3-4. コンディションオブジェクトの利用

変数でコンディションオブジェクトを受け取り、詳細情報を取得できる。

(handler-case
    (/ 10 0)
  (division-by-zero (c)             ; c にコンディションが入る
    (format t "エラー: ~A~%" c)
    :error))
;; エラー: arithmetic error DIVISION-BY-ZERO ...
;; → :ERROR

3-5. error 型ですべてのエラーを捕捉

(defun safe-eval (form)
  (handler-case
      (eval form)
    (error (e)                      ; すべてのエラーを捕捉
      (format t "エラーが発生: ~A~%" e)
      nil)))

(safe-eval '(+ 1 2))
;; → 3

(safe-eval '(/ 1 0))
;; エラーが発生: ...
;; → NIL

4. ignore-errors と unwind-protect

4-1. ignore-errors

エラーを無視して NIL を返す簡易的な方法。

;; エラーが発生すると NIL を返す
(ignore-errors (/ 10 0))
;; → NIL, #<DIVISION-BY-ZERO ...>(2つの値)

;; 正常な場合は結果を返す
(ignore-errors (/ 10 2))
;; → 5

注意 ignore-errors はデバッグを困難にするため、本番コードでは handler-case を推奨する。

4-2. unwind-protect

エラーが発生しても必ず実行したい処理がある場合に使う。

(unwind-protect
    保護される式
  後始末の式...)
;; ファイルを確実に閉じる
(let ((stream (open "test.txt" :direction :output)))
  (unwind-protect
      (progn
        (write-line "Hello" stream)
        (error "Something went wrong"))  ; エラー発生
    (close stream)                        ; 必ず実行される
    (format t "ストリームを閉じました~%")))

4-3. with-open-file との関係

with-open-file は内部で unwind-protect を使用している。

;; with-open-file は自動的にファイルを閉じる
(with-open-file (stream "test.txt" :direction :output)
  (write-line "Hello" stream)
  (error "Error!"))  ; エラーでもファイルは閉じられる

5. コンディションの種類

5-1. コンディションの階層

5-2. 主なコンディション型

コンディション 説明
error すべてのエラーの基底
simple-error 単純なエラー
type-error 型の不一致
arithmetic-error 算術エラー
division-by-zero ゼロ除算
file-error ファイル関連エラー
end-of-file ファイル終端
parse-error パースエラー
warning 警告(続行可能)

5-3. コンディションの判定

(handler-case
    (/ 10 0)
  (division-by-zero ()
    :division-by-zero)
  (arithmetic-error ()
    :arithmetic-error)
  (error ()
    :other-error))
;; → :DIVISION-BY-ZERO(最初にマッチしたもの)

注意 より具体的なコンディションを先に書く。

6. カスタムコンディション

6-1. define-condition

独自のコンディションを定義できる。

;; カスタムコンディションの定義
(define-condition invalid-age-error (error)
  ((age :initarg :age               ; スロット定義
        :reader invalid-age-error-age))
  (:report (lambda (c stream)       ; エラーメッセージの生成
             (format stream "Invalid age: ~A"
                     (invalid-age-error-age c)))))

;; 使用例
(defun set-age (age)
  (if (or (< age 0) (> age 150))
      (error 'invalid-age-error :age age)  ; カスタムエラーを発生
      age))

(handler-case
    (set-age -5)
  (invalid-age-error (e)
    (format t "エラー: ~A~%" e)))
;; エラー: Invalid age: -5

6-2. スロットを持つコンディション

エラー発生時の詳細情報(値、フィールド名、理由など)を格納する変数を持つカスタムコンディション。 捕捉側でスロットにアクセスし、エラーの原因を詳しく調べられる。

;; 複数のスロットを持つコンディション
(define-condition validation-error (error)
  ((field :initarg :field           ; フィールド名
          :reader validation-error-field)
   (value :initarg :value           ; 不正な値
          :reader validation-error-value)
   (reason :initarg :reason         ; 理由
           :reader validation-error-reason))
  (:report (lambda (c stream)
             (format stream "Validation error in ~A: ~A (~A)"
                     (validation-error-field c)
                     (validation-error-value c)
                     (validation-error-reason c)))))

;; 使用例
(defun validate-email (email)
  (unless (position #\@ email)
    (error 'validation-error
           :field "email"
           :value email
           :reason "missing @"))
  email)

(handler-case
    (validate-email "invalid")
  (validation-error (e)
    (format t "フィールド: ~A~%"
            (validation-error-field e))))
;; フィールド: email

7. リスタート

7-1. リスタートとは

リスタートは、エラー発生時に 復帰方法 を提供する仕組み。エラーを捕捉する側が、どのように復帰するか選択できる。

7-2. restart-case

(defun parse-integer-with-restart (str)
  (restart-case
      (let ((result (parse-integer str :junk-allowed nil)))
        (or result
            (error "Cannot parse: ~A" str)))
    (use-value (value)              ; リスタート1:値を指定
      :report "Use a specific value"
      :interactive (lambda ()
                     (format t "Enter value: ")
                     (list (read)))
      value)
    (use-zero ()                    ; リスタート2:0を使う
      :report "Use zero"
      0)))

;; 対話的に使用する場合、エラー時にリスタートを選択できる

7-3. invoke-restart

プログラム内からリスタートを呼び出す。

(defun safe-parse-integer (str)
  (handler-bind
      ((error (lambda (c)
                (declare (ignore c))
                (invoke-restart 'use-zero))))  ; use-zero リスタートを呼ぶ
    (parse-integer-with-restart str)))

(safe-parse-integer "abc")
;; → 0

7-4. 組み込みリスタート

いくつかのリスタートは標準で用意されている。

リスタート 説明
abort 処理を中止
continue 処理を続行
muffle-warning 警告を抑制
use-value 代わりの値を使用
store-value 値を格納して続行
;; abort リスタートを使用
(defun do-something ()
  (restart-case
      (progn
        (format t "処理開始~%")
        (error "エラー発生"))
    (abort ()
      :report "処理を中止"
      (format t "中止しました~%")
      nil)))

(handler-bind
    ((error (lambda (c)
              (declare (ignore c))
              (invoke-restart 'abort))))
  (do-something))
;; 処理開始
;; 中止しました
;; → NIL

8. handler-bind

スタックを巻き戻さずにコンディションを処理するマクロ。 エラー発生地点のコンテキストを保持したまま処理できるため、リスタートへのアクセスが可能。

8-1. handler-case との違い

機能 handler-case handler-bind
スタック巻き戻し 行う 行わない
リスタートへのアクセス 不可 可能
用途 単純なエラー処理 高度な制御

8-2. 使用例

;; handler-bind はスタックを巻き戻さずにハンドラを実行
(handler-bind
    ((warning (lambda (c)
                (format t "警告を検出: ~A~%" c)
                (muffle-warning))))  ; 警告を抑制
  (warn "This is a warning")
  (format t "処理を続行~%"))
;; 警告を検出: This is a warning
;; 処理を続行

9. 実践例

9-1. ファイル読み込みのエラー処理

(defun read-file-safely (path)
  "ファイルを安全に読み込み、エラー時はNILを返す"
  (handler-case
      (with-open-file (stream path :direction :input)
        (let ((contents (make-string (file-length stream))))
          (read-sequence contents stream)  ; ファイル内容を読み込む
          contents))
    (file-error (e)
      (format t "ファイルエラー: ~A~%" e)
      nil)
    (error (e)
      (format t "予期せぬエラー: ~A~%" e)
      nil)))

(read-file-safely "nonexistent.txt")
;; ファイルエラー: ...
;; → NIL

9-2. 数値入力のバリデーション

;; カスタムコンディション:不正な入力を表すエラー
(define-condition invalid-input (error)
  ((input :initarg :input                   ; 入力された値
          :reader invalid-input-value)
   (expected :initarg :expected             ; 期待される条件
             :reader invalid-input-expected))
  (:report (lambda (c stream)               ; エラーメッセージの生成
             (format stream "Invalid input: ~A (expected ~A)"
                     (invalid-input-value c)
                     (invalid-input-expected c)))))

;; 正の整数を読み込む。不正な場合は再入力を求める
(defun read-positive-integer ()
  (loop                                     ; 有効な入力があるまで繰り返す
    (format t "正の整数を入力: ")
    (force-output)                          ; 出力を即座に表示
    (let ((input (read-line)))              ; 1行読み込む
      (handler-case
          (let ((n (parse-integer input)))  ; 文字列を整数に変換
            (if (plusp n)
                (return n)                  ; 正の数なら値を返して終了
                (error 'invalid-input       ; 正でなければエラー
                       :input n
                       :expected "positive")))
        (parse-error ()                     ; 数値に変換できなかった
          (format t "数値を入力してください~%"))
        (invalid-input (e)                  ; 正の数でなかった
          (format t "~A~%" e))))))

9-3. リトライ機能付きHTTPリクエスト(擬似)

;; カスタムコンディション:ネットワークエラーを表す
(define-condition network-error (error)
  ((url :initarg :url                       ; アクセスしたURL
        :reader network-error-url))
  (:report (lambda (c stream)               ; エラーメッセージの生成
             (format stream "Network error accessing ~A"
                     (network-error-url c)))))

;; 最大max-retries回までリトライするHTTP GET
(defun http-get-with-retry (url &key (max-retries 3))
  (let ((attempts 0))                       ; 試行回数
    (loop
      (incf attempts)                       ; 試行回数を増やす
      (handler-case
          (return (http-get url))           ; 成功したら値を返す
        (network-error (e)
          (if (< attempts max-retries)
              (progn
                (format t "リトライ ~A/~A: ~A~%"
                        attempts max-retries e)
                (sleep 1))                  ; 1秒待機してリトライ
              (error e)))))))               ; 最大回数に達したらエラー

;; 擬似的なhttp-get(テスト用)
(let ((call-count 0))                       ; クロージャで呼び出し回数を保持
  (defun http-get (url)
    (incf call-count)
    (if (< call-count 3)
        (error 'network-error :url url)     ; 最初の2回は失敗
        (format nil "Response from ~A" url)))) ; 3回目以降は成功

9-4. トランザクション風の処理

;; 操作リストを実行し、エラー時はロールバック
(defun with-transaction (operations)
  (let ((completed nil))                    ; 完了した操作を記録するリスト
    (unwind-protect
        (handler-case
            (progn
              (dolist (op operations)       ; 各操作を順に実行
                (funcall op)
                (push op completed))        ; 成功した操作を記録
              :success)                     ; すべて成功
          (error (e)                        ; エラー発生時
            (format t "エラー発生: ~A~%" e)
            (format t "ロールバック中...~%")
            :failed))
      ;; unwind-protect: 成功・失敗どちらでも実行される
      (format t "完了した操作: ~A個~%" (length completed)))))

;; 使用例
(with-transaction
  (list (lambda () (format t "操作1 完了~%"))
        (lambda () (format t "操作2 完了~%"))
        (lambda () (error "操作3で失敗"))
        (lambda () (format t "操作4 完了~%"))))
;; 操作1 完了
;; 操作2 完了
;; エラー発生: 操作3で失敗
;; ロールバック中...
;; 完了した操作: 2個
;; → :FAILED

10. 練習課題

課題1:安全な除算

ゼロ除算時に NIL を返す関数を作れ。

(safe-divide 10 2)  ;; → 5
(safe-divide 10 0)  ;; → NIL

解答

(defun safe-divide (a b)
  (handler-case
      (/ a b)
    (division-by-zero ()            ; ゼロ除算を捕捉
      nil)))

(safe-divide 10 2)  ;; → 5
(safe-divide 10 0)  ;; → NIL

課題2:デフォルト値付き変換

文字列を整数に変換し、失敗時はデフォルト値を返す関数を作れ。

(parse-int-or "123" 0)   ;; → 123
(parse-int-or "abc" 0)   ;; → 0

解答

(defun parse-int-or (str default)
  (handler-case
      (parse-integer str)
    (error ()                       ; すべてのエラーを捕捉
      default)))

(parse-int-or "123" 0)   ;; → 123
(parse-int-or "abc" 0)   ;; → 0

課題3:範囲チェック付きエラー

値が範囲外の場合にカスタムエラーを発生させる関数を作れ。

(check-range 50 0 100)   ;; → 50
(check-range 150 0 100)  ;; → エラー

解答

(define-condition out-of-range (error)
  ((value :initarg :value :reader out-of-range-value)
   (min :initarg :min :reader out-of-range-min)
   (max :initarg :max :reader out-of-range-max))
  (:report (lambda (c stream)
             (format stream "~A is out of range [~A, ~A]"
                     (out-of-range-value c)
                     (out-of-range-min c)
                     (out-of-range-max c)))))

(defun check-range (value min max)
  (if (and (>= value min) (<= value max))
      value
      (error 'out-of-range :value value :min min :max max)))

(check-range 50 0 100)   ;; → 50
;; (check-range 150 0 100)  → エラー: 150 is out of range [0, 100]

課題4:リトライ機能

失敗時に指定回数リトライする関数を作れ。

(with-retry 3 (lambda () (random-fail)))

解答

(defun with-retry (max-attempts fn)
  (loop for attempt from 1 to max-attempts
        do (handler-case
               (return (funcall fn))        ; 成功したら値を返す
             (error (e)
               (if (< attempt max-attempts)
                   (format t "リトライ ~A/~A~%" attempt max-attempts)
                   (error e))))))           ; 最終回はエラーを投げる

;; テスト用関数
(let ((count 0))
  (defun random-fail ()
    (incf count)
    (if (< count 3)
        (error "Failed")
        "Success")))

(with-retry 5 #'random-fail)
;; リトライ 1/5
;; リトライ 2/5
;; → "Success"

課題5:ログ付きエラー処理

エラーをログに記録してから再スローする関数を作れ。

解答

(defparameter *error-log* nil)          ; エラーログ

(defun with-error-logging (fn)
  (handler-case
      (funcall fn)
    (error (e)
      (push (list (get-universal-time) e) *error-log*)  ; ログに記録
      (error e))))                                       ; 再スロー

;; 使用例
(ignore-errors
  (with-error-logging
    (lambda () (/ 1 0))))

;; ログを確認
(first *error-log*)
;; → (時刻 #<DIVISION-BY-ZERO ...>)

課題6:cleanup付き処理

成功・失敗に関わらずクリーンアップ処理を行う関数を作れ。

解答

(defun with-cleanup (setup-fn body-fn cleanup-fn)
  (funcall setup-fn)                    ; セットアップ
  (unwind-protect
      (funcall body-fn)                 ; 本体処理
    (funcall cleanup-fn)))              ; 必ずクリーンアップ

;; 使用例
(with-cleanup
  (lambda () (format t "セットアップ~%"))
  (lambda ()
    (format t "処理中~%")
    (error "失敗"))
  (lambda () (format t "クリーンアップ~%")))
;; セットアップ
;; 処理中
;; クリーンアップ
;; → エラー発生

11. まとめ

エラー処理の基本

機能 関数/マクロ 用途
エラー発生 error 致命的エラー
警告 warn 警告(続行可能)
エラー捕捉 handler-case 一般的なエラー処理
エラー無視 ignore-errors 簡易的なエラー無視
後始末 unwind-protect 確実なクリーンアップ

コンディションの階層

condition
├── warning
└── serious-condition
    ├── error(一般的なエラー)
    │   ├── type-error
    │   ├── arithmetic-error
    │   │   └── division-by-zero
    │   └── file-error
    └── storage-condition

handler-case vs handler-bind

特徴 handler-case handler-bind
スタック巻き戻し あり なし
リスタート利用 不可 可能
使いやすさ 簡単 高度

エラー処理の選択

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?