Restarter or tiny tips for restart feature in common lisp.

Meta notes.

対象読者

Confirmation.

Typical restart.

典型的なrestartは、例えば以下のようなものになるかと思われます。

(restart-case (the-form-that-may-fails)
  (ignore () nil))

Typical restart function.

上のrestartをプログラムから呼び出したい場合、handler-bindを使用してconditionに紐づけたハンドラ内でrestartを探して呼び出すという処理を行うと思います。

(handler-bind ((error (lambda (condition)
                        (let ((restart (find-restart 'ignore condition))) ; <--- restartを探して、
                          (when restart                                   ; <--- もしあったら、
                            (invoke-restart restart))))))                 ; <--- 呼び出す。
  ...)

関数にしてしまう事例も多かろうと存じます。

(defun ignore (condition)
  (let ((restart (find-restart 'ignore condition)))
    (when restart
      (invoke-restart restart))))

Issue.

Package lock violation.

処理系によってはパッケージはロックされていたりします。 上のリスタート関数IGNOREの定義は少なくともSBCLにおいてはパッケージがロックされているためエラーとなります。

ましてやrestartの名前をdeletereplaceにしたい場合、Common Lispの通常の関数名と衝突するのでそのような名前のリスタート関数は定義できません。

Increasing exported symbols.

名前衝突を回避しようと試みる場合、リスタート名が不自然なものとなったり過剰に長くなったりするおそれがあります。

また、それらの名前は通常エクスポートされるものです。 エクスポートされるシンボルが増えるのは(将来の自分を含む)エンドユーザーにとって学習コスト増という負担になります。

Proposal.

Restarter.

典型的なリスタート関数はどうせ同じ処理を行うのですからリスタート関数を作って返す関数を定義すれば良いのではないでしょうか。

(defun restarter (restart-name &rest args)
  (lambda (condition)
    (let ((restart (find-restart restart-name condition)))
      (when restart
        (apply #'invoke-restart restart args)))))

上記関数RESTARTERさえあればリスタート関数を定義する必要はなくなります。 先の例をRESTARTERを使う形で書き直すと以下のようになります。

(handler-bind ((error (restarter 'ignore)))
  ...)

Keyword symbol as a restart name.

リスタート名はシンボルである必要がありますが、それはただのIDとしてしか使われません。 なら、キーワードシンボルでも構わないとは思いませんか?

キーワードシンボルを使えばパッケージからエクスポートされるシンボルを減らせます。 それはそのまま(自分を含む)エンドユーザーの学習コストの減少に直結します。

たとえばbabelのように、たとえ頻繁に使うシンボルはstring-to-octetsoctets-to-stringの2種だとしても、パッケージからエクスポートされているシンボルが合計33個もあると、もうその時点で「うげぇ」となって学習に対するモチベーションが下がることがあるのは賢明な読者の皆様におかれましてはご承知のとおりのことと存じます。 「学習コストの減少」にはそのような心理的抵抗(苦手意識)の減少も重要であると考えます。

Common Lispの仕様がリスタートには対応するリスタート関数が定義されてあるものという設計になっているのが、ミスリーディングしているように思えます。 上に見たようにRESTARTER関数さえあればリスタート関数群は必要ありません。

なお、賢明な読者諸兄の中にはキーワードシンボルを使うと複数の異なるライブラリ間でリスタート名の衝突が起こりうる点を懸念される御仁もいらっしゃるかもしれません。 ですがリスタートはリスタート名のみで求まるものかというとさにあらず、conditionとセットで求まるものでございます。 conditionを適切に定義すればたとえリスタート名が衝突してもリスタート節自体は適切に選択しうるものと愚考する次第でございます。

Conclusion.

muffle-warningのようにリスタートが存在しないならprogram-errorとするようなものにも対応するためにはやはりリスタート関数自体を自前で書く必要は残りますが、それでも多くの場合はRESTARTER関数で対応できるかと思われます。

alexandriaあたりが導入してくれると嬉しい。