restartを始めよう
restart
とは何か?
ずっとよく分かっていませんでした。
でもCommon Lispを作って行けばおのずと理解できると思って、
一通り作っても依然としてrestart
が理解できませんでした。
invoke-restart
もrestart-bind
もC言語で作ったのに
いまだに理解できないとはどういう事なんだ、驚きです。
という事で勉強し直しです。
勘違いかもしれませんが、restart
ってとても難しいのでは?
技術が難しいのではなく、何のために存在しているのかという
基本的な考え方があまり表に出てきてないように思えます。
とくにcondition
との違いが分からなかった。
何で似たような機能が二つあるの?
説明します。
restart
はerror
の強化版です。
restart
はあなたと対話をして、問題を解決する機能です。
分かりますか?
restart
はあなたと対話したいのです。
エラーが発生したら、これをどう処理したいのかLispがあなたに問いかけ、
あなたはキーボードとかマウスとか、
方法は知りませんけど何らかの方法で答えなければなりません。
それがrestart
です。
エラー処理のもう一つの機能としてcondition
がありますが、
こちらはそもそも対話を目的としていません。
handler-bind
やhandler-case
で捕捉したりしなかったりするだけです。
でもrestart
は違います。
restart
は、あなたの今の意見を聞きたいんです。
実際にやってみましょう。
sbclで次のように入力してください。
* aaa
出力結果は下記の通り。
debugger invoked on a UNBOUND-VARIABLE in thread #<THREAD "main thread" RUNNING {1000560083}>: The variable AAA is unbound. Type HELP for debugger help, or (SB-EXT:EXIT) to exit from SBCL. restarts (invokable by number or by possibly-abbreviated name): 0: [CONTINUE ] Retry using AAA. 1: [USE-VALUE ] Use specified value. 2: [STORE-VALUE] Set specified value and use it. 3: [ABORT ] Exit debugger, returning to top level. (SB-INT:SIMPLE-EVAL-IN-LEXENV AAA #<NULL-LEXENV>) 0]
ほら意味わからないのがいっぱい出てきた。
しかし、これがrestart
です。
「restart
はinvoke-restart
で実行し、restart-bind
で捕捉できます。」
みたいな説明は全然やさしくありません。
「restart
とはaaa
と入力すると、変な画面が出てくるアレです。」
と覚えましょう。
では上から確認していきます。
まずはエラーの原因を見ましょう。
debugger invoked on a UNBOUND-VARIABLE in thread #<THREAD "main thread" RUNNING {1000560083}>: The variable AAA is unbound.
UNBOUND-VARIABLE
condition
が発生したと言っています。
入力したaaa
がunbound
だという事です。
まあそうでしょう、疑いようもありません。
そこで、問題を解決する手段として、 下記の選択肢がありますと、Lispがあなたに問いかけます。
restarts (invokable by number or by possibly-abbreviated name): 0: [CONTINUE ] Retry using AAA. 1: [USE-VALUE ] Use specified value. 2: [STORE-VALUE] Set specified value and use it. 3: [ABORT ] Exit debugger, returning to top level.
さあ、どれを選ぶ?
この選択肢ごとに、restart
オブジェクトが用意されています。
あなたはどのrestart
オブジェクトを実行したいのか選ばなければなりません。
0
のcontinue
はもう一回やるという事なので、たぶん失敗するからやめます。
1
のuse-value
を選んでみましょう。
0] 1 Enter a form to be evaluated:
入力待ちになりました。
例えば100
と入力します。
Enter a form to be evaluated: 100 100 *
入力後、100
が返却されました。
つまり、unbound
のaaa
の代わりに100
が返却されたという事です。
こういう事もできます。
* (+ aaa bbb) debugger invoked on a UNBOUND-VARIABLE in thread #<THREAD "main thread" RUNNING {1000560083}>: The variable AAA is unbound.
aaa
がunbound
だと文句が来ました。
restarts (invokable by number or by possibly-abbreviated name): 0: [CONTINUE ] Retry using AAA. 1: [USE-VALUE ] Use specified value. 2: [STORE-VALUE] Set specified value and use it. 3: [ABORT ] Exit debugger, returning to top level. 0] 1
そこでuse-value
を選択肢し、100
を入力します。
Enter a form to be evaluated: 100 debugger invoked on a UNBOUND-VARIABLE in thread #<THREAD "main thread" RUNNING {1000560083}>: The variable BBB is unbound.
続いてbbb
がunbound
だと文句が来ました。
restarts (invokable by number or by possibly-abbreviated name): 0: [CONTINUE ] Retry using BBB. 1: [USE-VALUE ] Use specified value. 2: [STORE-VALUE] Set specified value and use it. 3: [ABORT ] Exit debugger, returning to top level. 0] 1
同じようにuse-value
を選択し、200
を入力します。
Enter a form to be evaluated: 200 300 *
その結果、300
が返却されました。
つまり、入力に
* (+ aaa bbb)
と指定したのですが、aaa
もbbb
もどちらもunbound
のため、
restart
が順番に2つ実行され、
use-value
により(+ 100 200)
が実行されたという事です。
足し算の結果は300
です。
このように、エラーが起こったにもかかわらず、
適切に修正して何事も無かったように動作を進めることを、
再起動、またはrestart
と呼びます。
どうしてもプロンプトが必要
restart
は対話することが仕事なので、
どうしたってあなたの意見を聞きだす機能が必要になります。
しかし、そのような機能があんまり無いんです。
標準で用意されている関数は次の二つです。
Y-OR-N-P
YES-OR-NO-P
どちらもyes
かno
かを聞き出すものです。
それはそれでとてもありがたいのですが、
もっと汎用的な入力を受け付ける関数は無いでしょうか?
残念ながらありません。
読む必要はありませんが、cltl2では次のような記載があります
Common Lisp the Language, 2nd Edition
29.3.6. Anonymous Restarts
https://www.cs.cmu.edu/Groups/AI/html/cltl/clm/node321.html
The question of whether or not prompt-for (or something like it) would be a useful addition to Common Lisp is under consideration by X3J13, but as of January 1989 no action has been taken. In spite of its use in a number of examples, nothing in the Common Lisp Condition System depends on this function.
prompt-for
という機能が欲しかったのかもしれません。
結局prompt-for
はANSI Common Lispには採用されませんでした。
でもrestart
にはかなり必要な機能だと思うのです。
例文から考えるに、prompt-for
はこんな感じに実装されているのだと思います。
(defun prompt-for (type prompt &optional (stream *query-io*)) (finish-output stream) (format stream "~&~A" prompt) (finish-output stream) (clear-input stream) (let ((x (read stream))) (fresh-line stream) (unless (typep x type) (error (make-condition 'type-error :datum x :expected-type type))) x))
実行してみます。
* (prompt-for 'integer "Hello: ") Hello: 100 100 * (prompt-for 'integer "Hello: ") Hello: aaa -> type-error
インターネットで検索してみると、
入力関数としてはprompt-read
というものが有名のようです。
Practical Common Lisp
http://www.gigamonkeys.com/book/
Practical: A Simple Database
http://www.gigamonkeys.com/book/practical-a-simple-database.html
(defun prompt-read (prompt) (format *query-io* "~a: " prompt) (force-output *query-io*) (read-line *query-io*))
こんな感じで使えます。
* (concatenate 'string "<<" (prompt-read "Input") ">>") Input: aaa "<<aaa>>"
いいですね。
処理系にも、sbcl, clisp, cclの内部の関数を見る限りだと、
似たような機能が用意されているのだと思います。
確認したいなら、こんな感じで調査できます。
* (apropos 'prompt) ・・・
これらを参考に、どうやってユーザーから 値を取得するのか考えておきましょう。
使用例
最後にrestart
とprompt-for
を使った実装例を示します。
絶対に偶数しか認めないevenp-force
関数を示します。
(defun evenp-force (x) (unless (evenp x) (error "~S is not an even number." x)) x)
こんな感じで使います。
(evenp-force 100) -> 100 (evenp-force 101) -> error
では、restart
を使って、use-value
を実装してみましょう。
use-value
とは、代わりに使う値を入力してもらうものです。
修正したevenp-force
は下記の通り。
(define-condition evenp-condition (simple-error) ()) (defun evenp-force (value) (tagbody loop (restart-bind ((use-value (lambda (x) (setq value x) (go loop)) :interactive-function (lambda () (list (prompt-for 'integer "Input value: "))) :report-function (lambda (stream) (format stream "Input value")))) (unless (evenp value) (error (make-condition 'evenp-condition :format-control "~S is not an even number." :format-arguments (list value)))) (return-from evenp-force value))))
ものすごくぐちゃぐちゃしていて汚いです。
でもちゃんと機能するので使ってみましょう。
* (evenp-force 10) 10 * (evenp-force 11) debugger invoked on a EVENP-CONDITION in thread #<THREAD "main thread" RUNNING {1000560083}>: 11 is not an even number. ・・・ 0: [USE-VALUE] Input value 0] 0 Input value: 101 ・・・ debugger invoked on a EVENP-CONDITION in thread #<THREAD "main thread" RUNNING {1000560083}>: 101 is not an even number. ・・・ 0: [USE-VALUE] Input value 0] 0 Input value: 200 200 *
うまく行きました。
最後にいらない話題をしておきます。
restart
は、当然自動化できます。
あれだけrestart
はユーザーと対話するものだと言っておきながら、
実はプログラムで自動的に値を選択することができます。
そうするために、本処理とinteractive
をわざわざ別々にしているのです。
だって自動化が無いとテストするときに面倒すぎるでしょう?
例を示します。
(handler-bind ((evenp-condition (lambda (c) (use-value 2222 c)))) (evenp-force 11)) -> 2222
restart-bind
ではなくhandler-bind
なので注意。
この書き方は、warning
を無視するときと同じ構文なのは分かるでしょうか。
warning
の場合はこんな感じで記載します。
(handler-bind ((warning #'muffle-warning)) (warn "Hello"))
以前の投稿warnの出力抑制はどうやって実現しているのか - nptclのブログでは、
どうしてこんな書き方をするのかわからなかったのですが、
restart
を自動的に処理するための典型的な構文だったようですね。
終わりです
中途半端に感じるかもしれませんが終わりです。
自分にとってはとても苦労して覚えたものです。
この考え方さえ押さえれば、あとは自力で勉強して行けると思います。
とても簡単なことしか書かれてないと思うかもしれませんが、
この考え方が無かったため、長い間restart
の意味が分かりませんでした。
でも仕様書を見るとちゃんと書いてあるんですよね。
それにも関わらず分からなかった。
なんか書いてること難しいし。
同じような人がいるかもしれないので投稿しておきます。