コンディションの定義と再試行マクロwith-ntimes-retry

証券会社に売買注文を出す処理は失敗することもあるので、再試行できるようにする必要がある。ある処理の中で特定のコンディションが通知されたとき、一定回数再試行するマクロを書くことにした。

まずコンディションを定義する必要があるが、コンディションの定義はCLOSのクラス定義とそっくりである。

(define-condition click-repl-error (simple-error)
  ((session :accessor session-of :initarg :session))
  (:report (lambda (condition stream)
	     (format stream "CLICK-REPL-ERROR: ~A" condition))))

第一引数にコンディションの名前、第二引数に親コンディション、第三引数はスロットのリストである。普通のクラスと同じように親コンディションのスロットを継承する。ここではsessionが唯一のスロットだ。オプショナル引数はエラーが通知されるときのメッセージを指定したりするらしい。
コンディションを通知するには、まずmake-condition (make-instanceと似ている)でコンディションを生成し、そのコンディションをerrorに渡す。

(error (make-condition 'click-repl-error :session cl-session)))

次にあるコンディションを捕捉して再試行するマクロを書く。これはn回連続でそのコンディションが出たときにはじめてエラーを出す

(defmacro with-ntimes-retry (condition-type n &body body)
  (let ((itr (gensym))
	(i   (gensym))
	(c   (gensym)))
    `(nlet ,itr ((,i 1))
       (if (not (= ,i 1))
	   (format t "; caught condition ~A: retrying by WITH-NTIMES-RETRY [~A/~A]~%"
		   ',condition-type (1- ,i) ,n))
       (if (> ,i ,n)
	   (error ',condition-type)
	   (handler-case
	       (progn ,@body)
	     (,condition-type (,c) ,c (,itr (1+ ,i))))))))

実行例は以下のようになる。再試行の上限は3回までとしているので、ゼロ除算を3回連続で行うとwith-ntimes-retryはそのコンディションをエラーとして通知する。

;;; 例
CL-USER> (ignore-errors
	   (with-ntimes-retry division-by-zero 3
	     (princ "10/?: ")
	     (/ 10 (read))))
10/?: 0
; caught condition DIVISION-BY-ZERO: retrying by WITH-NTIMES-RETRY [1/3]
10/?: 0
; caught condition DIVISION-BY-ZERO: retrying by WITH-NTIMES-RETRY [2/3]
10/?: 0
; caught condition DIVISION-BY-ZERO: retrying by WITH-NTIMES-RETRY [3/3]
NIL
#<DIVISION-BY-ZERO {10032F6AC1}>

逆に、3回以内にゼロ以外の数で割れば、DIVISION-BY-ZEROは通知されないので再試行の結果がwith-ntimes-retryの返り値となる。

CL-USER> (ignore-errors
	   (with-ntimes-retry division-by-zero 3
	     (princ "10/?: ")
	     (/ 10 (read))))
10/?: 0
; caught condition DIVISION-BY-ZERO: retrying by WITH-NTIMES-RETRY [1/3]
10/?: 1
10

それにても、激しく車輪の再発明っぽい機能である。