warnの出力抑制はどうやって実現しているのか
warn
の出力を抑制する方法
(handler-bind ((warning #'muffle-warning)) (warn "Hello"))
やった!
warn
関数とその周辺の実装を考える
Common Lispのwarning
の話題です。
warn
関数では警告を出力できますが、
その出力を抑止したい場合はどうしたらいいでしょうか。
先行して回答を示したように、handler-bind
とmuffle-warning
を組み合わせて
設定することでwarn
の出力を抑止することができます。
つまり、単純に
(warn "Hello")
と実行すると、
WARNING: Hello
みたいな出力が出るのですが、
(handler-bind ((warning #'muffle-warning)) (warn "Hello"))
では何も出力されません。
これは一体どのような仕組みで実行されているのでしょうか?
handler-bind
とmuffle-warning
という組み合わせから、
condition
とrestart
が手を組んでいることがわかると思います。
たかがwarning
抑止だと舐めてかかれるようなモノじゃなく、
なんか知らんが結構複雑ですので、ちゃんと説明していきます。
muffle-warning
とは何か
muffle-warning
はrestart
関数と呼ばれています。
最後に使い方を説明しますが、warning
を中断させる機能があります。
restart
関数は他にもあり、abort
関数やらcontinue
関数がそれにあたります。
restart
関数の内容は、多少の差異はあるものの大体決まっており、
find-restart
とinvoke-restart
の2つにより構成されています。
muffle-warning
の実装例である、muffle-warning!
を下記に示します。
(defun muffle-warning! (&optional condition) (let ((restart (find-restart 'muffle-warning! condition))) (if restart (invoke-restart restart) (error (make-condition 'control-error)))))
たぶんどの処理系でもこんな感じに実装されていると思います。
find-restart
で探して、見つかったならそれをinvoke
するというもの。
つまりは、warning
というcondition
を受け取ったら、
muffle-warning
より、再起動関数が呼び出されることになります。
逆にいうなら、warning condition
をsignal
で呼び出す前には、
必ずrestart-bind
かrestart-case
にてmuffle-warning restart
を用意してあげなければ
control-error
になってしまいます。
この要件が、まずはwarn
関数を作成する際に必要になります。
注意点としては、restart
関数は自分の関数名と同じ名前のrestart
を探します。
今の場合、muffle-warning
関数は、muffle-warning
という名前のrestart
を
探してinvoke
しているのです。
関数名とrestart
の名前はわざと同じにしているのですが、
同じsymbol
でも無関係であることを覚えておいた方がいいと思います。
自作のwarning condition
を作る
テスト用にwarning condition
を作りましょう。
要件は1つ。format
の2要素である、format
文字列と引数を受け取れることです。
slot
を作るのが面倒なのでsimple-condition
を継承します。
名前はwarning!
とします。
(define-condition warning! (simple-condition) ()) (defmethod print-object ((instance warning!) stream) (let ((format (simple-condition-format-control instance)) (args (simple-condition-format-arguments instance))) (apply #'format stream format args)))
print-object method
は、例えばprinc
なんかで引数として与えられたときに、
どういう文字列を出力するかを指定するものです。
深く考えずに、warning!
が受け取った引数から、format
をそのまま出力してやります。
例えばこんな感じ。
(let ((inst (make-condition 'warning! :format-control "HELLO" :format-arguments nil))) (format t "~S~%" inst)) → HELLO
自作のwarn
関数を作る
では、上記の要件から、自作のwarn
関数である、warn!
関数を作成することを考えます。
要件は2つ。
muffle-warning! restart
を用意する。warning! condition
を実行する。
warn!
関数の実装は下記のようになります。
(defun warn! (format &rest args) (restart-case (signal (make-condition 'warning! :format-control format :format-arguments args)) (muffle-warning! ())))
それではwarn!
関数を次のように実行した場合はどうなるでしょうか。
(handler-bind ((warning! #'muffle-warning!)) (warn! "Hello"))
warn!
関数は、まずはmuffle-warning! restart
を用意したあと、
warn! instance
に出力内容を設定して、signal
でcondition
を呼び出します。
するとhandler-bind
により、muffle-warning!
関数が呼び出されます。
muffle-warning!
関数はinvokeによりwarn!
関数に戻り、restart-case
が実行されます。
restart-case
の本体には何も記載がないものの、
bind
ではなくcase
であるため、warn!
関数に制御がそのまま残り、
そして何もしないでwarn!
関数が終了します。
流れは次のような感じになります。
warn!
起動signal
によりhandler-bind
のwarning!
起動muffle-warning!
関数実行restart-case
のmuffle-warning!
起動restart-case
終了warn!
終了
これは本来のwarn
関数の挙動と同じになります。
それではhandler-bind
無しでwarn!
を呼び出した場合はどうなるでしょうか。
残念ながらこのままでは、warning! condition
のhandler
が存在しないため、
signal
関数が何もしないで終わってしまいます。
warn!
関数が警告文字を出力するためには、
出力用のhandler-bind
が必要となります。
出力用handler-bind
を用意する
これはプログラマーが明示的に用意するものではなく、
Common Lisp処理系があらかじめ用意しておくものです。
warning!
を出力するための、システムのコードを示します。
(handler-bind ((warning! (lambda (c) (format *error-output* "WARNING-TEST: ~A~%" c)))) ;; code )
上記の;; code
の部分で、eval-loop
であったり、
load
関数だったりが動作するわけです。
この要件から、warn!
関数は
(warn! "Hello")
としていたものは
(handler-bind ((warning! (lambda (c) (format *error-output* "WARNING-TEST: ~A~%" c)))) (warn! "Hello"))
みたいな感じになります。
warn!
関数がsignal
を起動するため、handler-bind
に制御が渡って、
format
関数によりWARNING-TEST
が出力されます。
一方、出力抑止である
(handler-bind ((warning! #'muffle-warning!)) (warn! "Hello"))
は、
(handler-bind ((warning! (lambda (c) (format *error-output* "WARNING-TEST: ~A~%" c)))) (handler-bind ((warning! #'muffle-warning!)) (warn! "Hello")))
となります。
warn!
関数はsignal
によりhandler-bind
を起動しますが、
内側のhandler-bind
に制御が渡り、mufffle-warning!
関数が呼び出されるため、
一番外側のhandler-bind
には制御が渡らず、WARNING-TEST
が出力されないのです。
この仕組みは、warning! condition
を途中で盗み見することができることを意味しています。
例えば次のコードを考えます。
(handler-bind ((warning! (lambda (c) (format *error-output* "<<<~A>>>~%" c)))) (warn! "Hello"))
出力結果
<<<Hello>>> WARNING-TEST: Hello
用意したhandler-bind
で一度condition
を受け取ってから、
format
で<<<Hello>>>
を出力させています。
condition
はそのまま外側に伝搬するので、
WARNING-TEST: Hello
も出力されるわけです。
それでは、これ以上伝搬させたくない場合はどうしたらいいでしょうか?
そんな時に使用するのがmuffle-warning
関数です。
(handler-bind ((warning! (lambda (c) (format *error-output* "<<<~A>>>~%" c) (muffle-warning! c)))) (warn! "Hello"))
出力結果
<<<Hello>>>
テストコード
説明で作成した自作のwarn!
関数を下記に示します。
(defun muffle-warning! (&optional condition) (let ((restart (find-restart 'muffle-warning! condition))) (if restart (invoke-restart restart) (error (make-condition 'control-error))))) (define-condition warning! (simple-condition) ()) (defmethod print-object ((instance warning!) stream) (let ((format (simple-condition-format-control instance)) (args (simple-condition-format-arguments instance))) (apply #'format stream format args))) (defun warn! (format &rest args) (restart-case (signal (make-condition 'warning! :format-control format :format-arguments args)) (muffle-warning! ()))) (defmacro progn-warning (&body body) `(handler-bind ((warning! (lambda (c) (format *error-output* "~&WARNING-TEST: ~A~%" c)))) ,@body)) ;; ;; main ;; (progn-warning (warn! "AAA") (warn! "BBB: ~A, ~A, ~A" 100 200 300) (handler-bind ((warning! #'muffle-warning!)) (warn! "CCC") (warn! "DDD")) (warn! "EEE"))
出力例
WARNING-TEST: AAA WARNING-TEST: BBB: 100, 200, 300 WARNING-TEST: EEE
clispだと2回出力されるのですが何故だろう。
無視します。