Design by contract

http://d.hatena.ne.jp/cranebird/20080322/1206197358 で気にしたような、コンパイラだとかインタプリタだとかのような複雑なプログラムをちゃんと実装するには、何らかの手法が必要。


何となく「契約による設計」という言葉を思い出した。知っているのは不変条件、事前条件、事後条件という言葉くらいだけど。
macro を使えば、関数定義の際に常に条件を指定させることが可能だ。それにまともに使ったことはないけど、 CLOS のメソッドコンビネーションを使えば、対象とするメソッドの実行前後に、チェック用のメソッドを走らせることができる。
以下はスタッククラスに事前条件事後条件をつけてみたもの。push したら要素数は1増えていなければならないし、pop したら要素数は減っていなくてはならない、ということをプログラムにした。ただし、実行前と実行後とで比較しなくてはいけない時に、その値を渡す方法が分からなかったので、格好悪いがクラス本体にダミーのスロットを追加している。あとはこの種の条件を指定することを強制できるように、 defclass もどきを作れば良い。

;; クラス定義
(defclass stack ()
  ((items :accessor items :initform nil)
   (pre :accessor pre :initform nil)))

(defmethod stack-size ((st stack))
  (length (items st)))

(defmethod stack-empty ((st stack))
  (= 0 (stack-size st)))

(defmethod stack-push ((st stack) item)
  (push item (items st)))
;; push 実行前のスタックのサイズを保存
(defmethod stack-push :before ((st stack) item)
  (declare (ignore item))
  (setf (pre st) (stack-size st)))
;; push 実行後にスタックのサイズが増えていることを確認
(defmethod stack-push :after ((st stack) item)
  (declare (ignore item))
  (assert (= (1+ (pre st)) (stack-size st))))

(defmethod stack-pop ((st stack))
  (pop (items st)))

;; pop の事前条件
(defmethod stack-pop :before ((st stack))
  (assert (not (stack-empty st)) (st))
  (setf (pre st) (stack-size st)))

;; pop の事後条件
(defmethod stack-pop :after ((st stack))
  (assert (= (pre st) (1+ (stack-size st)))))

と、試してみてから冷静に考えると、他の誰かとっくの昔に Design by contract を実装していない訳はないのだ。あっさり見つけたのがこれ。

コンディション(例外のようなもの)システムを組み込んでいるし、 define-method-combination で独自のメソッドコンビネーションを定義しているし、独自の defclass も定義している。自分が思いつくものの数段上。エレガントだし、CLOS はパワフルだなぁ。後で試してみる。