prompt-forを完成させる
前回の投稿で登場したprompt-for
に
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))
これでも全然悪くないのですけど、
type
による確認が異常の時は、ただtype-error
を発生させるのではなく、
restart
で解決案をいくつか提示したいと思います。
改善後のprompt-for
を下記に示します。
(define-condition prompt-for-error (type-error) ()) (defun prompt-for (type prompt &optional (stream *query-io*)) (format stream "~&~A" prompt) (finish-output stream) (clear-input stream) (prog ((value (read stream))) (fresh-line stream) retry (restart-bind ((continue (lambda () (go retry)) :report-function (lambda (s) (format s "Retry type check."))) (use-value (lambda (x) (setq value x) (go retry)) :interactive-function (lambda () (list (prompt-for t "Input value: " stream))) :report-function (lambda (s) (format s "Use an another value")))) (if (typep value type) (return value) (error (make-condition 'prompt-for-error :datum value :expected-type type))))))
変わったのは下記のrestart
を用意したという点です。
continue
use-value
continue
は、型の確認をもう一度実行するというものです。
再実行するだけなので、たぶんtype error
が生じるだけですが、
type
をdeftype
で変更したり、
あるいはsatisfies
関数の挙動を変更した場合は
再実行に意味があるかもしれません。
use-value
は、もう一度入力してもらい、
代わりにその値を使用するというものです。
誤入力の場合もあると思いますので、
このrestart
はとても親切だと思います。
実際にやってみましょう。
まずは正常終了の場合です。
* (prompt-for 'integer "Hello: ") Hello: 100 100 *
100
が返却されました。
続いて異常時でのuse-value
の使用例です。
* (prompt-for 'integer "Hello: ") Hello: zzz debugger invoked on a PROMPT-FOR-ERROR in thread #<THREAD "main thread" RUNNING {1000560083}>: The value ZZZ is not of type INTEGER 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 type check. 1: [USE-VALUE] Use an another value 2: [ABORT ] Exit debugger, returning to top level. (PROMPT-FOR INTEGER "Hello: " #<SYNONYM-STREAM :SYMBOL *TERMINAL-IO* {1000024C83}>) source: (ERROR (MAKE-CONDITION 'PROMPT-FOR-ERROR :DATUM VALUE :EXPECTED-TYPE TYPE)) 0] 1 Input value: 999 999 *
ちゃんと機能しています。
use-value
で入力した値が型に違反していた場合でも、
ちゃんとやり直しをすることができます。
* (prompt-for 'integer "Hello: ") Hello: zzz debugger invoked on a PROMPT-FOR-ERROR in thread #<THREAD "main thread" RUNNING {1000560083}>: ・・・ restarts (invokable by number or by possibly-abbreviated name): 1: [USE-VALUE] Use an another value 0] 1 Input value: qqqq debugger invoked on a PROMPT-FOR-ERROR in thread #<THREAD "main thread" RUNNING {1000560083}>: ・・・ restarts (invokable by number or by possibly-abbreviated name): 1: [USE-VALUE] Use an another value 0] 1 Input value: 123 123 *
次はエラーの捕捉を見てみましょう。
方法は次の2通り存在します。
(handler-case (prompt-for 'integer "Integer: ") (prompt-for-error () 999)) (handler-bind ((prompt-for-error (lambda (c) (use-value 999 c)))) (prompt-for 'integer "Integer: "))
どちらも入力で受け取った値が型integer
に違反した場合は999
を返却します。
しかし処理の方法が違っています。
hander-case
の場合は、condition
を捕捉してから999を返却しており
prompt-for
に戻ることはありません。
つまりprompt-for
を中断して処理を続行しています。
それに対してhandler-bind
の場合は、
condition
を捕捉したあとに、use-value
関数を用いて
prompt-for
のrestart
のuse-value
を実行しています。
prompt-for
をやり直すという意味があります。
handler-case
の例の場合は、
返却値に整数以外を設定することができますが、
handler-bind
の例はuse-value
に整数以外の値を設定すると
prompt-for
に戻ったときにまたエラーが発生するので
永久に処理が終わらなくなってしまいます。