2009年1月18日日曜日

【実践CL】19 例外処理を越えて:コンディションと再起動

事情によりスキップして19章へ。

  • こんなシチュエーションを想定しているようだ。

    (defun high (...)
    ...
    (medium ...)
    ...)

    (defun medium (...)
    ...
    (low ...)
    ...)

    (defun low (...)
    ...))

  • lowが失敗し、mediumがそれに対応できないことがありえるとする。highをどうしておくべきか。
  • 選択肢1:原因にしたがって、mediumが担当すべきだった作業含めてhighが実施。これはmediumの機能をほぼhighがもっていることになり、なんだか、という点と、この例のような三階層じゃなくて階層が増えたらhighに詰む機能が膨大になるという課題がある。
  • 選択肢2:パッチをあてる。再度mediumがlowを呼び出したときにlowがエラーを出さないようにする処理をhighが実施して再度mediumを呼出す。これも、mediumの内部仕様についてhighを書く人が熟知していることになるので、関数のブラックボックス性から言うと好ましくない。
  • なんかうまい方法があるの? あるよ。それがCLのコンディションシステム。

  • 19.1 Lispのやり方

    • ログを解析する仮想的なソフトウエアを例にLispのやり方を説明する。
    • 状況設定。

      Webサーバのログのようなテキスト形式のログファイルを読むソフトウエア。

      parse-log-entry:
      入力:ひとつのログエントリを含むテキスト
      出力:log-entryオブジェクト

      parse-log-file:
      入力:ひとつのログファイル
      出力:log-entryオブジェクトのリスト


  • 19.2 コンディション

    • 基礎情報。

      • conditionはstandard-objectの系列ではない。
      • conditionオブジェクトは、make-conditionで生成する。
      • conditionオブジェクトの中身へのアクセスはslot-valueではアクセスできない。
      • conditionオブジェクトの定義には、define-conditionを使う。

    • ここの状況での定義。

      ;; parse-log-entryがパースできないテキストを与えられた時に通知するコンディション
      (define-condition malformed-log-entry-error (error)
      ((text :initarg :text :reader text)))


  • 19.3 コンディションハンドラ

    • コンディションの通知はerrorによって実施する。
    • errorの呼び出し方法は2つ。
    • 1:事前にインスタンス化したconditionオブジェクトをerrorの引数にする。
    • 2:コンディションクラスの名前とそれへの引数をerrorの引数にする。
    • ここの状況での定義。

      ;; parse-log-entryのアウトライン
      (defun parse-log-entry (text)
      (if (woll-formed-log-entry-p text)
      (make-instance 'log-entry ...)
      (error 'malformed-log-entry-error :text text)))


    • コンディションが発行されたときに、何が起こるかは、コールスタック上で発行地点(parse-log-entry)より下に何があるかによる。
    • デバッガに入らないためには、どこかでコンディションハンドラを確立している必要がある。
    • コンディションハンドラは、コンディションハンドラ自体とそれを取扱う処理系部分とで理解すべき。
    • コンディションハンドラの基本事項は次のとおり。

      • コンディションハンドラは、取扱対象コンディションの型指定子と、コンディションを引数とする関数から構成される。
      • コンディションハンドラは、コードのどこかで処理系に登録され、アクティブ状態になる。
      • コンディションハンドラ内関数で、該当コンディションを処理しないこともできる。その場合、処理系に制御は戻る。

    • 処理系は次のとおり。

      • コンディションが発生した場合、アクティブなコンディションハンドラのリストを探索して、その型に対応するコンディションハンドラを呼ぶ。
      • 同じ型に対するコンディションハンドラが複数ある場合は、スタック上の距離で、コンディション発生地点に近いものが呼ばれる。
      • 呼んだコンディションハンドラが処理を拒否した場合は、次のハンドラが存在すれば、それを呼ぶ。
      • コンディションハンドラを呼んだ時点ではスタックは巻き戻らない。処理の流れをどうするかはユーザが決めることだ。

    • handler-caseは、コンディションハンドラを作成し登録する。そして処理系は、conditionが発生した場合、スタックをコンディションハンドラ登録ポイントまで巻き戻し、コンディションハンドラを実行する。
    • ここの状況での定義。

      (defun parse-log-file (file)
      (with-open-file (in file :direction :input)
      (loop for text = (read-line in nil nil) while text
      for entry = (handler-case (parse-log-entry text)
      (malformed-log-entry-error () nil))
      when entry collect it)))

    • これはparse-log-fileとしては、やりすぎである。というのは、無視することを決めこんでいるからだ。不正なエントリがあったときにどうすべきかは、parse-log-fileを使う関数側で指定できるべきだろう。

  • 19.4 再起動

    • これを実現するには、parse-log-fileでは、condition発生時に実行可能なrestartの選択肢を登録するにかぎるようにする。
    • それにはrestart-caseを使う。
    • ここの状況での定義。

      (defun parse-log-file (file)
      (with-open-file (in file :direction :input)
      (loop for text = (read-line in nil nil) while text
      for entry = (restart-case (parse-log-entry text)
      (skip-log-entry () nil))
      when entry collect it)))

    • さて、ここで例として、parse-log-fileの上位に位置する(parse-log-fileを利用する)関数を2つ定義する。

      (defun log-analyzer ()
      (dolist (log (find-all-logs))
      (analyze-log log)))

      (defun analyze-log (log)
      (dolist (entry (parse-log-file log))
      (analyze-entry entry)))

    • これらの中で、malformed-...が発行された場合の対処を指定したい。handler-caseを使うと、そこまで巻き戻ってしまう。
    • そこで、handler-bindを使う。log-analyzerの中で定義する。

      (defun log-analyzer ()
      (handler-bind ((malformed-log-entry-error
      #'skip-log-entry))
      (dolist (log (find-all-logs))
      (analyze-log log))))

      (defun skip-log-entry (c)
      (let ((restart (find-restart 'skip-log-entry)))
      (when restart (invoke-restart restart))))


  • 19.5 複数の再起動を提供する

    • 最下層のparse-log-entryに、2つのrestartを導入する。

      (defun parse-log-entry (text)
      (if (woll-formed-log-entry-p text)
      (make-instance 'log-entry ...)
      (restart-case (error 'malformed-log-entry-error :text text)
      (use-value (value) value)
      (reparse-entry (fixed-text) (parse-log-entry fixed-text)))))

    • このうちのuse-valueを上位で指定して使うとしたら、例えば次のようだ。

      (defun log-analyzer ()
      (handler-bind ((malformed-log-entry-error
      #'(lambda (c)
      (use-value
      (make-instance 'malformed-log-entry :text (text c))))))
      (dolist (log (find-all-logs))
      (analyze-log log))))


  • 19.6 コンディションの別な使い道

    • errorとwarnとcerrorの差異。うーん。よくわからん。


自分なりに、こつこつ。

0 件のコメント: