nptclのブログ

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

restartを始めよう

restartとは何か?
ずっとよく分かっていませんでした。
でもCommon Lispを作って行けばおのずと理解できると思って、 一通り作っても依然としてrestartが理解できませんでした。
invoke-restartrestart-bindC言語で作ったのに いまだに理解できないとはどういう事なんだ、驚きです。
という事で勉強し直しです。

勘違いかもしれませんが、restartってとても難しいのでは?
技術が難しいのではなく、何のために存在しているのかという 基本的な考え方があまり表に出てきてないように思えます。
とくにconditionとの違いが分からなかった。
何で似たような機能が二つあるの?

説明します。
restarterrorの強化版です。
restartはあなたと対話をして、問題を解決する機能です。
分かりますか?
restartはあなたと対話したいのです。
エラーが発生したら、これをどう処理したいのかLispがあなたに問いかけ、 あなたはキーボードとかマウスとか、 方法は知りませんけど何らかの方法で答えなければなりません。
それがrestartです。

エラー処理のもう一つの機能としてconditionがありますが、 こちらはそもそも対話を目的としていません。
handler-bindhandler-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です。

restartinvoke-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が発生したと言っています。
入力したaaaunboundだという事です。
まあそうでしょう、疑いようもありません。

そこで、問題を解決する手段として、 下記の選択肢がありますと、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オブジェクトを実行したいのか選ばなければなりません。

0continueはもう一回やるという事なので、たぶん失敗するからやめます。
1use-valueを選んでみましょう。

0] 1

Enter a form to be evaluated:

入力待ちになりました。
例えば100と入力します。

Enter a form to be evaluated: 100
100
*

入力後、100が返却されました。
つまり、unboundaaaの代わりに100が返却されたという事です。

こういう事もできます。

* (+ aaa bbb)

debugger invoked on a UNBOUND-VARIABLE in thread
#<THREAD "main thread" RUNNING {1000560083}>:
  The variable AAA is unbound.

aaaunboundだと文句が来ました。

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.

続いてbbbunboundだと文句が来ました。

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)

と指定したのですが、aaabbbもどちらもunboundのため、 restartが順番に2つ実行され、 use-valueにより(+ 100 200)が実行されたという事です。
足し算の結果は300です。

このように、エラーが起こったにもかかわらず、 適切に修正して何事も無かったように動作を進めることを、 再起動、またはrestartと呼びます。

どうしてもプロンプトが必要

restartは対話することが仕事なので、 どうしたってあなたの意見を聞きだす機能が必要になります。

しかし、そのような機能があんまり無いんです。
標準で用意されている関数は次の二つです。

  • Y-OR-N-P
  • YES-OR-NO-P

どちらもyesnoかを聞き出すものです。
それはそれでとてもありがたいのですが、 もっと汎用的な入力を受け付ける関数は無いでしょうか?

残念ながらありません。
読む必要はありませんが、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-forANSI 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)
・・・

これらを参考に、どうやってユーザーから 値を取得するのか考えておきましょう。

使用例

最後にrestartprompt-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の意味が分かりませんでした。
でも仕様書を見るとちゃんと書いてあるんですよね。
それにも関わらず分からなかった。
なんか書いてること難しいし。
同じような人がいるかもしれないので投稿しておきます。