Common Lispのrestart特性

4096 ワード

主流のプログラミング言語では、エラーを表す手段は2つにすぎません.
  • 関数呼び出し戻りエラーコード
  • 関数呼び出し放出異常
  • C言語は前者に属し、fopen(3)関数はファイルを正常に開くとFILEポインタを返し、失敗するとNULLを返し、エラーコードをグローバル変数errnoに書き込む.Ruby言語は後者に属し、File.Openメソッドはファイルが見つからないときにENOENTの異常を投げ出し、File.Openの外にbeginを包んで...rescue...endはその放出された異常を捕捉することができる.
    Common Lispのエラーメカニズムを状況システム(Condition System)と呼び,異常メカニズムと同様に放出異常(関数errorを用いる)と捕捉異常を実現できる(マクロhandler-caseを使用しています).しかし、他の言語の異常メカニズムとは異なり、状況システムにはRESTARTという特性があり、使用者やコードによって必要かどうかを決定し、エラーからどのように回復するかを決定することができます.ユニークで奇妙に聞こえますが、下を見続けてみてはいかがでしょうか.
    ログをファイルに記録する機能を提供するWebフレームワークがあるとします.この機能は、init-loggerという関数を呼び出して初期化する必要があります.この関数は、次のように実現されます.
    (defun init-logger ()
      (with-open-file (s "/tmp/log/web.log"
                         :direction :output
                         :if-exists :supersede)
        (format s "Logger module starting...")))
    

    この関数はフレームワークの初期化コードによって呼び出され、次のようになります.
    (defun init-framework ()
      (format t "Framework starting...~%")
      (init-plugin))
    
    (defun init-plugin ()
      (format t "Plugins starting...~%")
      (init-logger))
    

    フレームワークの初期化コードは、次のようにアプリケーションの初期化コードによって呼び出されます.
    (defun init-app ()
      (format t "Application starting...~%")
      (init-framework))
    

    ディレクトリ/tmp/logが存在しない場合にinit-app関数を呼び出すと、/tmp/log/web.logというファイルが見つからないというエラーが表示されます.エラーがアプリケーションの通常の起動プロセスを中断しないように、init-app関数にログファイルを格納するディレクトリの作成を担当させることができます.Init-app関数を次の形式に書き換える
    (defun init-app ()
      (format t "Application starting...~%")
      (ensure-directories-exist "/tmp/log/")
      (init-framework))
    

    この方法は確かに実行可能であるが、アプリケーション層のコード(init-app関数)は、最下位にあるログモジュールの実装の詳細を理解しなければならない.直感的に、フレームワークのことはフレームワーク自体がソリューションを提供して解決すべきであることを教えてくれた.Common Lispのrestart機能を利用して、フレームワークは確かに外部にソリューションを提供することができる.
    まず、ログファイルを格納するディレクトリが存在するかどうかをアクティブに検出する必要があります.UIOPというパッケージを利用して、次のコードを書くことができます.
    (defun init-logger ()
      (let ((dir "/tmp/log/"))
        (unless (uiop:directory-exists-p dir)
          (error 'file-error :pathname dir))
        (with-open-file (s (format nil "~Aweb.log" dir)
                           :direction :output
                           :if-exists :supersede)
          (format s "Logger module starting..."))))
    

    次に、init-loggerは、呼び出し者にディレクトリに存在しない問題の解決策を積極的に提供する必要があります.1つの方法は、このディレクトリが存在しない場合、呼び出し者が作成するかどうかを選択することです.Common Lispのrestart-caseマクロを使用して、init-loggerを次のように書き換えました.
    (defun init-logger ()
      (let ((dir "/tmp/log/"))
        (restart-case
            (unless (uiop:directory-exists-p dir)
              (error 'file-error :pathname dir))
          (create-log-directory ()
            :report (lambda (stream)
                      (format stream "Create the directory ~A" dir))
            (ensure-directories-exist dir)))
        (with-open-file (s (format nil "~Aweb.log" dir)
                           :direction :output
                           :if-exists :supersede)
          (format s "Logger module starting..."))))
    

    このとき、第1版のinit-app関数を呼び出すと、init-loggerは例外(file-errorのタイプは以前と同じ)を投げ出してSBCL(SBCLを使用しています)のデバッガに持ち込みますが、見た内容は少し異なります.
    error on file "/tmp/log"
       [Condition of type FILE-ERROR]
    
    Restarts:
     0: [CREATE-LOG-DIRECTORY] Create the directory /tmp/log ;; )
    

    Restartsにcreate-log-directoryという新しい項目が追加されました.これはinit-loggerでrestart-caseで定義された新しい「リカバリ対策」です.0を入力してこのrestartをトリガーすると、Common Lispは定義されたrestart-caseマクロに対応する句に戻ってテーブル式を実行します.つまり、CL:ENSURE-DIRECTORY-EXIST関数を呼び出して/tmp/logを作成します.
    常にCREATE-LOG-DIRECTORYというオプションを実行してログファイルを格納するディレクトリを作成したい場合は、Common Lispを使用するhandler-bindinvoke-restartの関数を組み合わせるだけで、コードに直接指定できます.最終的なinit-app関数の実現は次のようになります.
    (defun init-app ()
      (format t "Application starting...~%")
      (handler-bind
          ((file-error #'(lambda (c)
                           (declare (ignorable c))
                           (invoke-restart 'create-log-directory))))
        (init-framework)))