nptclのブログ

Common Lisp処理系nptの開発メモです。https://github.com/nptcl/npt

prompt-forを完成させる

前回の投稿で登場したprompt-forrestartをつけて豪華にしてみましょう。
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が生じるだけですが、 typedeftypeで変更したり、 あるいは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-forrestartuse-valueを実行しています。
prompt-forをやり直すという意味があります。

handler-caseの例の場合は、 返却値に整数以外を設定することができますが、 handler-bindの例はuse-valueに整数以外の値を設定すると prompt-forに戻ったときにまたエラーが発生するので 永久に処理が終わらなくなってしまいます。