形式手法とアジャイル
私は形式手法にはソフトウェアの品質を高める力があると信じている。一方、プロジェクトの進め方に関して言えばアジャイルのようなやり方が有効だと思っている。しかし、私が形式手法とアジャイルの両方に興味を持つ事に関して、そういう人は珍しいと言う意見を何度か耳にした。どうもアジャイルと形式手法が相容れないと考える人が多いようである。その気持ちは何となく分かるが、私としては両立すると思っているので、今日はその事を書きたい。
私の主張は以下の通りである。
アジャイルなプロジェクトで形式手法を活用する事ができる。
それに対して、よく(?)聞かれる反対意見は以下のようなものである。
- アジャイルなプロジェクトでは、前払いの設計を行わない。形式手法を適応する場はないのではないか?
- 設計で形式手法を使った場合通常より時間がかかるのではないか?その結果、開発の速度を落とすのでは?
- テスト駆動開発で開発しているから検証は十分ではないか?
- 形式手法のツールを使いこなすのが難しい。学習に時間がかかる。
- 証明までしなくても良いのでは?過剰品質では?
以下、それぞれの反対意見に反論してみよう。
1. アジャイルなプロジェクトでは、前払いの設計を行わない。形式手法を適応する場はないのではないか?
「アジャイルなプロジェクトでは、前払いの設計を行わない」と言われているが、まったく設計を行わないわけではない。確かに設計書とかアーキテクチャ説明書のような文書は作らないかもしれないが、プログラマが実装する(まさにソースコードを編集する)前には頭の中で設計という行為が行われている。そこでは、メソッドや変数の名前を決めたり、クラス間の関係を考えたり、クラスの状態遷移を思い浮かべシミュレーションしたりする。これらはその結果を文書の形で残さなくても立派な設計行為である。そして、この設計した結果を検証するのに形式手法を用いる事ができる。特に振る舞いを検証するときには形式手法のツールが役に立つだろう。
2. 設計で形式手法を使った場合通常より時間がかかるのではないか?その結果、開発の速度を落とすのでは?
確かに設計に費やす時間が増える可能性はある。しかし、仮に設計に多くの時間がかかったとしても従来のやり方では設計段階で見つけられなかった不具合を見つける事ができれば、デバッグと修正にかかる時間が削減できるためトータルでは時間を短くすることができる。形式手法やツールについて不慣れなうちは設計にかかる時間が従来に比べてかなり多くなるだろうが、それは経験を重ねることによって徐々に改善される。私は何度か実プロジェクトでモデル検査を使用した事があるが、以前マルチスレッドのライブラリにおける関数呼び出しとコールバックについてデッドロックが起きないかをSPINで検証したときは、スクラッチからモデルをつくって検証を終えるのに約1時間であった。同じことをテストでやったとしても同程度の時間がかかるのではないかと思う。しかもテストではなかなか網羅的にテストすることは難しいが、モデル検査ならば全ケースを網羅的に調べてくれる。
3. テスト駆動開発で開発しているから検証は十分ではないか?
十分ではない(ことがある)。理由は、網羅的にテストをすることは(主に時間の都合で)難しく、その結果不具合を見逃す事があるからである。テスト駆動開発で書かれるテストは決して十分ではない。テスト駆動開発のテストは単体テストとは異なり、網羅して不具合を出し切ることが目的ではなく、実装にあたり大きな考え違いはないかを見つけたり、使用者にとって使いやすい仕様になっているか確認したり、そして実装者に少しだけの自信を与えるたりするものである。検証という点では十分とは言いがたい。
4. 形式手法のツールを使いこなすのが難しい。学習に時間がかかる。
確かにそういう点はある。しかし、先日IPAで開催されたVDMのセミナーによると一週間の教育期間をもうければ、あとはエキスパートの助言を受けながら開発をすることはできるという。そのセミナーでも言われていたのだが、自分のものにするには教科書の問題である程度練習したら、実践することが一番である。実際のプロジェクトで頭をフル回転させながら挑めば、あっという間に上達するだろう。そして上達した暁には、以前より正確なプログラムを速く開発できるようになる。ということでこの意見に対しては、最初は時間はかかるだろうけど、そのうち学習時間を取り消すくらいの効果がある。というのが私の意見である。
5. 証明までしなくても良いのでは?過剰品質では?
確かにプログラムの正当性を証明するとなるとツールのサポートがあったとしても時間がかかるだろう。そしてその時間と結果として担保される品質を天秤にかけたとき、過剰品質となる場合はある。しかし、形式手法だからといって必ずプログラムの正当性を証明しないといけない訳ではない。たとえば、モデル検査は有限の状態空間がある性質を保持するかを検査する方法で、証明よりもずっと手軽にできる。マルチスレッドやマルチプロセスのシステムでは、テストではタイミングに依存するため不具合を捕捉しきれないことがあるが、モデル検査を使えば網羅的に検査する事ができる。またAlloyというツールを使えば、仕様に暗黙の前提を置いていないかとか抜けや漏れがないかを確かめる事ができる。それぞれの手法(ツール)には得意とするところがあり、求める品質と対象のシステムの特徴をよく考えて、適した手法を用いれば過剰品質という状態は避けられるだろう。
以上の議論から私を主張をまとめると以下の通りである。
アジャイル開発においても形式手法を用いる場面はある。ただし、最初の学習段階ではエキスパートの助けがあった方がいいことと問題に適した手法を採用することが大事である。
2012年に形式手法を学び始めるならこの7冊
前エントリー「僕が形式手法を学び始めたときに読んだ10冊 - masaterukの日記」のラインナップはあまりに入手困難なものばかりだったので、2012年に始めるならということで改めて選んでみた。
1冊目。
VDM++によるオブジェクト指向システムの高品質設計と検証 (IT architects’ archive)
- 作者: ジョン・フィッツジェラルド、ピーター・ゴルム・ラーセン、ポール・マッカージー、ニコ・プラット、マーセル・バーホフ,酒匂寛
- 出版社/メーカー: 翔泳社
- 発売日: 2010/08/03
- メディア: 大型本
- 購入: 1人 クリック: 32回
- この商品を含むブログ (8件) を見る
2冊目。
- 作者: Daniel Jackson,中島震,今井健男,酒井政裕,遠藤侑介,片岡欣夫
- 出版社/メーカー: オーム社
- 発売日: 2011/07/15
- メディア: 単行本(ソフトカバー)
- 購入: 8人 クリック: 274回
- この商品を含むブログ (35件) を見る
3冊目。
- 作者: Mordechai Ben-Ari,中島震,谷津弘一,野中哲,足立太郎
- 出版社/メーカー: オーム社
- 発売日: 2010/03/24
- メディア: 単行本(ソフトカバー)
- 購入: 4人 クリック: 43回
- この商品を含むブログ (2件) を見る
4冊目。
オブジェクト指向入門 第2版 原則・コンセプト (IT Architect’Archive クラシックモダン・コンピューティング)
- 作者: バートランド・メイヤー,酒匂寛
- 出版社/メーカー: 翔泳社
- 発売日: 2007/01/10
- メディア: 単行本(ソフトカバー)
- 購入: 11人 クリック: 307回
- この商品を含むブログ (132件) を見る
5冊目。
Bメソッドによる形式仕様記述―ソフトウェアシステムのモデル化とその検証 (トップエスイー実践講座)
- 作者: 来間啓伸,中島震
- 出版社/メーカー: 近代科学社
- 発売日: 2007/12
- メディア: 単行本
- 購入: 1人 クリック: 13回
- この商品を含むブログ (4件) を見る
6冊目。
Concurrent and Real-time Systems: The CSP Approach (Worldwide Series in Computer Science)
- 作者: Steve Schneider
- 出版社/メーカー: John Wiley & Sons
- 発売日: 1999/11/05
- メディア: ペーパーバック
- クリック: 11回
- この商品を含むブログを見る
7冊目。
Logic in Computer Science: Modelling and Reasoning about Systems
- 作者: Michael Huth,Mark Ryan
- 出版社/メーカー: Cambridge University Press
- 発売日: 2004/07/31
- メディア: ペーパーバック
- クリック: 5回
- この商品を含むブログ (1件) を見る
ということで、今回は以上7冊。
僕が形式手法を学び始めたときに読んだ10冊
後輩に形式手法を学び始めたときに読んだ本を紹介すると約束したので、せっかくなのでブログに書くことにした。
形式手法を学び始めたのは2004年だから、今から始める場合はまた違ったラインナップになるだろうけど。
※「2012年に形式手法を学び始めるならこの7冊 - masaterukの日記」を書きました。
1冊目。
- 作者: D.グリース,筧捷彦
- 出版社/メーカー: 培風館
- 発売日: 1991/01
- メディア: 単行本
- クリック: 20回
- この商品を含むブログを見る
2冊目。
- 作者: ジョンフィッツジェラルド,ペーター・ゴルムラーセン,John Fitzgerald,Peter Gorm Larsen,荒木啓二郎,荻野隆彦,染谷誠,張漢明,佐原伸
- 出版社/メーカー: 岩波書店
- 発売日: 2003/02/26
- メディア: 単行本
- クリック: 4回
- この商品を含むブログ (7件) を見る
3冊目。
- 作者: B.ポター,D.ティル,J.シンクレア,Ben Potter,David Till,Jane Sinclair,田中武二
- 出版社/メーカー: トッパン
- 発売日: 1993/09
- メディア: 単行本
- クリック: 5回
- この商品を含むブログ (4件) を見る
4冊目。
- 作者: 田辺誠,中島玲二,長谷川真人
- 出版社/メーカー: 岩波書店
- 発売日: 1999/09/28
- メディア: 単行本
- 購入: 1人 クリック: 19回
- この商品を含むブログ (2件) を見る
当時は、上記4冊がいわゆる必須科目と言われていた。この4冊は何度も読み返した。
5冊目。
- 作者: 戸田山和久
- 出版社/メーカー: 名古屋大学出版会
- 発売日: 2000/10/10
- メディア: 単行本
- 購入: 27人 クリック: 330回
- この商品を含むブログ (108件) を見る
6冊目。
オブジェクト指向入門 (ASCII SOFTWARE SCIENCE Programming Paradigm)
- 作者: Bertrand Meyer,酒匂寛,酒匂順子
- 出版社/メーカー: アスキー
- 発売日: 1990/11
- メディア: 単行本
- 購入: 1人 クリック: 80回
- この商品を含むブログ (68件) を見る
7冊目。
- 作者: C.A.R.ホーア,C.A.R. Hoare,吉田信博
- 出版社/メーカー: 丸善
- 発売日: 1992/03
- メディア: 単行本
- 購入: 1人 クリック: 32回
- この商品を含むブログ (4件) を見る
8冊目。
- 作者: ジェラルド・ジェイサスマン,ジュリーサスマン,ハロルドエイブルソン,Gerald Jay Sussman,Julie Sussman,Harold Abelson,和田英一
- 出版社/メーカー: ピアソンエデュケーション
- 発売日: 2000/02
- メディア: 単行本
- 購入: 35人 クリック: 1,149回
- この商品を含むブログ (480件) を見る
9冊目。
SPIN Model Checker, The: Primer and Reference Manual
- 作者: Gerard J. Holzmann
- 出版社/メーカー: Addison-Wesley Professional
- 発売日: 2003/09/04
- メディア: ハードカバー
- クリック: 3回
- この商品を含むブログ (7件) を見る
10冊目。
Software Abstractions: Logic, Language, and Analysis
- 作者: Daniel Jackson
- 出版社/メーカー: The MIT Press
- 発売日: 2006/03/24
- メディア: ハードカバー
- 購入: 1人 クリック: 21回
- この商品を含むブログ (8件) を見る
以上、10冊。
多相型をサポートしました ー OCamlをC言語に変換するプログラム ー
OCamlをC言語に変換するプログラム(今のところ、uCamlって名前にしてます)に機能追加しました。多相型のサポートです。
https://github.com/masateruk/micro-caml
これにより、以下のようなOCamlのプログラムをCのプログラムに変換できます。
let rec id x = x in let n = if (id true) then (id 1) else (id 0) in print_int n
多相型のサポートによる恩恵は、上記プログラムにある id という関数です。これは引数 x をそのまま返すなんてことない関数ですが、注目してほしいのはその型です。型を調べるためにOCamlのインタプリンタに入力してみましょう。
$ ocaml Objective Caml version 3.12.0 # let rec id x = x;; val id : 'a -> 'a = <fun>
val id : 'a -> 'a =
# id 0;; - : int = 0 # id true;; - : bool = true
こういった関数をC言語で実現するのは簡単ではありません。たいてい型の制約をさけるために void* を利用しますが、これだと引数を関数に適応する場合にキャストする必要があります。キャストした場合はコンパイラの型検査の恩恵をあずかることができません。たとえば、int 型の引数を適応した結果をbool 型の変数に代入するようなコードがあってもキャストしている場合はコンパイラはエラーを報告してくれません。しかし、OCaml(だけでなく一般的な関数型言語)ならば、こういった型エラーをコンパイル時に報告してくれます。uCamlでもOCamlと同様の型検査を行います。型検査を通ったものがC言語に変換されるため、生成されたC言語のプログラムには型エラーはありません。
一番上にあげたプログラムは以下の手順で C へ変換、コンパイル、実行できます。
$ ./ucaml id.ml -o id.c $ gcc -Wall id.c -o id -I/usr/local/include -I. -lgc -L/usr/local/lib $ ./id 1
ひとつだけ注意点があります。今回は、メモリ管理の部分をさぼって Boehm GC (http://www.hpl.hp.com/personal/Hans_Boehm/gc/) を使うようにしました。よって、生成した C のプログラムをビルドおよび実行するときには Boehm GC が必要になります。個人的には自分でメモリ管理するよりもこういった GC を利用した方がいいと思っているのですが、一応 Boehm GC に依存しないものも出力できるようにするつもりです。
OCamlをC言語に変換するプログラムをつくりました
MinCamlをベースにしてOCamlをC言語に変換するプログラムをつくりました。
https://github.com/masateruk/micro-caml
何でそんなものをつくったかと言うと、わたしは組み込みプログラムの開発を仕事にしているのですが、
ということで、自分が書くときはOCamlで書いて、納品するときにCに変換すればいいんじゃないかと思った次第です。OCamlを書ける人がいるんだったらOCamlのコードを引き継いでもらえばいいけど、Cしかわかりませんな人には変換後のCのコードを渡すという作戦です。もちろん、変換後のCのコードを直接変更しちゃうとOCamlのコードは役に立たなくなるんですけど、それはしょうがないとします。
サポートしている構文要素はMinCamlよりもさらに少なく、基本型はInt型のみ。破壊的代入もありません。しかしクロージャはサポートしたので、以下のようなプログラム
let rec plus x = let rec add y = x + y in add in print_int ((plus 1) 2)
は次のようなCのプログラムに変換します。
#include <stdio.h> #include <stdbool.h> int _add_11_(int y_12_, int x_6_) { return x_6_ + y_12_; } typedef struct { int (*f)(int, int); int fv1; } c_13_t; c_13_t make_c_13_t(int (*f)(int, int), int fv1) { c_13_t tmp_r14; tmp_r14.f = f; tmp_r14.fv1 = fv1; return tmp_r14; } c_13_t plus_5_(int x_6_) { c_13_t add_11_; add_11_ = make_c_13_t(_add_11_, x_6_); return add_11_; } int apply_c_13_t(c_13_t tmp_r15, int tmp_i16) { return tmp_r15.f(tmp_i16, tmp_r15.fv1); } int main() { int tmp_i1_10_; c_13_t tmp_f2_8_; int tmp_i3_9_; int tmp_i4_7_; tmp_i1_10_ = 1; tmp_f2_8_ = plus_5_(tmp_i1_10_); tmp_i3_9_ = 2; tmp_i4_7_ = apply_c_13_t(tmp_f2_8_, tmp_i3_9_); printf("%d", tmp_i4_7_); return 0; }
テンポラリーな変数が多いのは、おそらくK正規化のためだと思います。変換先の言語がCなので、K正規化を適当にサボればもう少し読みやすいコードが出力できるようになるかと思います。
まだまだ機能が足りないのですぐに実用できるレベルではありませんが、入出力やリソース管理を除いたロジックを取り出して、その部分をささっとつくっちゃうようなことができないかと期待してます。
今後、他の言語要素もサポートしたいのですが、単純にOCamlの言語要素をサポートするのではなくモデリングやプロトタイプ時に役に立つような機能をたそうかなと考えているところです。
ねらいは、OCamlでモデリング(プロトタイプや検証など)してからC言語を生成です。
コマンドキューをもつシステムのAlloyモデル
とあることがきっかけで、コマンドキューをもつシステムをAlloyでモデル化し検証したので、ここで紹介します。
問題
あるシステムのミドルウェアを修正することになりました。このミドルウェアは整数値をやり取りするオブジェクトを複数所有します。インタフェースは以下の通りです。
- assign(v) // オブジェクトの値をvに上書きします。
- copy(obj) // オブジェクトobjの値をコピーします。
- getValue() // オブジェクトの値を取得します。
このミドルウェアを実装するためにライブラリを使用しています。このライブラリはオブジェクトを複数所有しています。各オブジェクトはキューを持ち、コマンドをキューに積んでから実行する必要があります。コマンドは2種類あります。
- assign(v)コマンドは、値の上書きする。
- copy(obj)コマンドは、オブジェクトobjの値をコピーする。
キューに積んだコマンドを実行するときはライブラリの関数
- execute()
を呼びます。executeを呼ぶとキューに積まれた順番にコマンドを実行します。オブジェクトの値は、これまたライブラリの関数
- get()
でいつでも(同期で)読みだせます。
当初の実装は以下のように、コマンドをひとつ積むたびにexecute()を呼びだすようになっていました。
void assign(int v) { add_command(make_command(assign, v)); execute(); }
しかしexecuteに時間がかかっていたので、なるべくコマンドをためてからexecuteをするように改良しようとなりました。
はじめは思いついたままに実装してテストをしていたのですが、テストの種類を増やすとうまくいかないものがぽつぽつ見つかるという状態が続いたので、Alloyで検証することにしました。以下では、モデルを検証しながらインクリメンタルに作った過程を再現します。
状態モデル
まずは、値、コマンド、オブジェクトをモデル化します。コマンドキューを表すのには seq を使いました。後々検査で使うために、オブジェクトが保持する値は、実際の値(real)と期待値(expexted)のふたつにします。
module CommandQueue abstract sig Value {} one sig Zero, One, Two extends Value {} abstract sig Command {} sig Copy extends Command { src : one Object } sig Assign extends Command { v : one Value } sig Object { expected : one Value, real : one Value, q : seq Command } pred show() {} run show for 3 but exactly 1 Object
オブジェクトの数をひとつに制限して、[Execute]、[Show] の順番に実行します。以下のようなインスタンスが見つかりました。
[Next]を押して次々に出てくるインスタンスを確認します。想定外のインスタンスが見つからないので、ここまでは正しそうです。
assignとcopyを追加する
次に、assignコマンドとcopyコマンドをキューに積む操作を追加します。まずは、状態の変化を表すためにTimeシグネチャのorderingを導入します。
open util/ordering[Time] as to sig Time {}
時間で変化する属性をTimeとの関係にします。
sig Object { expected : Value one -> Time, real : Value one -> Time, q : (seq Command)-> Time }
時間変化をTimeを右側に持ってきた関係で表すので、多重度をシグネチャの右側に移動させます。コマンドキューが時間によって変化するので、seq Command 全体を括弧で囲みます。
assignコマンドは述語で表します。t が事前、t' が事後の時点を表します。この時点では期待値(expected)だけ変更し、実際の値は変更しません。
pred assign [t, t' : Time, obj : Object] { some assign : Assign | obj.q.t' = (obj.q.t).add[assign] and obj.expected.t' = assign.v and noChangeRealValue[t, t', obj] }
述語 noChangeRealValue は以下の通り。いろんなところで使いまわしそうなのでひとつの pred として切りだしました。
pred noChangeRealValue [t, t' : Time, obj : Object] { obj.real.t' = obj.real.t }
copyコマンドもほぼ同様です。
pred copy [t, t' : Time, obj : Object] { some cp : Copy | obj.q.t' = (obj.q.t).add[cp] and obj.expected.t' = cp.src.expected.t and noChangeRealValue[t, t', obj] }
最後にこのシステムがとり得る状態遷移空間をfactで指定します。状態遷移空間は、初期状態から assign または copy コマンドによって遷移できる空間です。
pred init [t : Time] { all obj : Object | (no obj.q.t and obj.real.t = obj.expected.t) } pred noChangeAll [t, t' : Time, obj : Object] { obj.expected.t' = obj.expected.t obj.real.t' = obj.real.t obj.q.t' = obj.q.t } fact step { init[to/first[]] all t : Time - to/last[] | let t' = to/next[t] | one obj : Object | // (a) (assign[t, t', obj] or copy[t, t', obj]) and (all obj' : Object | obj' != obj => noChangeAll[t, t', obj']) // (b) }
fact step の (a) で one obj : Object、および (b) で (all obj' : Object | ...) としているのは、1ステップで状態遷移するオブジェクトをひとつに限定するためです。そうしておくとVisializeしたときに何が起きたか分かりやすくなります。ただし、同じTimeインスタンス数では、同時に複数遷移するモデルに比べて探索できる空間が狭くなるので、見つかってほしい反例が見つかりにくくなります。
今度はObjectのインスタンス数を 2 にして[Show]してみます。
pred show() {} run show for 3 but exactly 2 Object
妙なインスタンスが見つからないので、ここまでは正しそうです。
executeを追加する
execute も述語で表します。
pred execute [t, t' : Time, obj : Object] { isEmpty[obj.q.t'] let command = last[obj.q.t] { (command in Assign) => (obj.real.t' = command.v and obj.expected.t' = obj.expected.t) (command in Copy) => (obj.real.t' = command.src.real.t and obj.expected.t' = obj.expected.t) } }
これは、「キューは空で、キューの最後に積まれているのが assign だったら execute 後の real は expexted と同じ。copy だったら src の real と同じ。かつ、expected は変化しない。」です(察しのいい人は気付くと思いますが、これは間違っています)。
次に、fact step に execute を追加します。
fact step { init[to/first[]] all t : Time - to/last[] | let t' = to/next[t] | one obj : Object | (assign[t, t', obj] or copy[t, t', obj] or execute[t, t', obj]) and (all obj' : Object | obj' != obj => noChangeAll[t, t', obj']) }
最初のモデル ー get前にexecuteする ー
一応、これで準備は整いました。では、executeの回数を減らした新しいミドルウェアの振る舞いを考えて、それを検証したいと思います。最初に考えたのは、以下のようにassignやcopyのメソッド内でexecuteを一切呼ばないことにして、getValueが呼ばれたときにgetする前にexecuteするようにしたらどうかと考えました。
void assign(int v) { add_command(make_command(assign, v)); // execute(); // <- 毎回呼ぶのを止める } int getValue() { execute(); // <- get前に一度だけ呼ぶように。 return get(); }
これはいつでもexecuteを呼べば、そのあとは正しい値を取得できるということを根拠にしています。よって、このことをアサートで検査してみます。execute 後は期待値と実際の値が一致するというアサートは以下のように書けます。
assert RealValueEqualsExpectedAfterExecute { all t : Time - to/last[] | let t' = to/next[t] | all obj : Object | execute[t, t', obj] => obj.real.t' = obj.expected.t' } check RealValueEqualsExpectedAfterExecute for 3 but exactly 1 Object, 4 Time, 4 seq
まずはオブジェクト1個に限定して、[Execute]メニューからアサートを選んで実行します。そうすると以下のような反例が見つかります。
- 初期状態は、Object.real = Object.expected = Two
- Object.assgin(Zero). Object.real = Two, Object.expected = Zero
- Object.copy(Object). Object.real = Two, Object.expected = Zero
- Object.execute. Object.real = Two, Object.expected = Zero
これは、初期状態から、assign, copy(自分自身), execute と進んだ場合、assign で代入した値が無視されて期待値と実際の値が異なることを示しています。実際のシステムは execute した場合、キューのコマンドを順番に実行するわけですから、キューの先頭に入っている assign を実行した時点で期待値と実値が一致します。よって、そのあとに copy を実行すれば、copy 後も期待値と実値は一致します。ということは、execute のモデルが間違っていると言えます。
executeを修正する
では、executeを修正しましょう。今回は素朴にキューのすべてのコマンドをひとつずつ実行するように修正します。「ひとつずつ実行する」といっても、Alloyではループや再帰呼び出しを書けません。そこで、コマンドひとつを実行するたびにひとつ時点を進めるようにします。つまり、ある時点 t でコマンドキューに assign, copy と積まれていた状態で execute すると、t の次の時点(これを t' とします)ではassign実行後の状態、t' の次の時点ではcopy実行後の状態に遷移するとします。実際のシステムでは、execute はアトミックに実行されます。よって、モデル上でも execute しはじめたらキューが空になるまでの間はコマンドの追加できないようにします。これを実現するためにモデルに状態を表す変数を追加します。
sig State {} one sig InitDone, AssignAdded, CopyAdded, Executing, ExecuteDone extends State {} sig Object { expected : Value one -> Time, real : Value one -> Time, q : (seq Command)-> Time, state : State one -> Time }
noChangeAll にも state の項を追加します。
pred noChangeAll [t, t' : Time, obj : Object] { obj.expected.t' = obj.expected.t obj.real.t' = obj.real.t obj.q.t' = obj.q.t obj.state.t' = obj.state.t }
初期状態を InitDone にします。
pred init [t : Time] { all obj : Object | (no obj.q.t and obj.real.t = obj.expected.t and obj.state.t = InitDone) }
Executing状態の間は assign, copy コマンドが実行できないように pred に事前条件を追加します。加えて、事後の状態をそれぞれAssignAdded, CopyAddedにします。
pred assign [t, t' : Time, obj : Object] { no obj' : Object | obj'.state.t = Executing // pre-condition some assign : Assign { obj.q.t' = (obj.q.t).add[assign] obj.expected.t' = assign.v noChangeRealValue[t, t', obj] obj.state.t' = AssignAdded } } pred copy [t, t' : Time, obj : Object] { no obj' : Object | obj'.state.t = Executing // pre-condition some cp : Copy { obj.q.t' = (obj.q.t).add[cp] obj.expected.t' = cp.src.expected.t noChangeRealValue[t, t', obj] obj.state.t' = CopyAdded } }
次にキューの先頭にあるコマンドをひとつだけ実行する pred を書きます。
pred executeCommand [t, t' : Time, obj : Object] { let command = first[obj.q.t] { (command in Assign) => (obj.real.t' = command.v and obj.q.t' = rest[obj.q.t] and obj.expected.t' = obj.expected.t) (command in Copy) => (obj.real.t' = command.src.real.t and obj.q.t' = rest[obj.q.t] and obj.expected.t' = obj.expected.t) (isEmpty[obj.q.t'] => obj.state.t' = ExecuteDone) and (not isEmpty[obj.q.t'] => obj.state.t' = Executing) } }
executeCommand を使うと execute 時にひとつだけコマンドを実行する pred は以下のように書けます。
pred execute [t, t' : Time, obj : Object] { some q.t // pre-condition no obj : Object | obj.state.t = Executing // pre-condition executeCommand[t, t', obj] }
execute 時にキューにふたつ以上のコマンドがある場合は、続けて残りのコマンドをする必要があります。それも pred で表します。
pred executeRest [t, t' : Time, obj : Object] { obj.state.t = Executing // pre-condition executeCommand[t, t', obj] }
あとは fact step に executeRest を追加するだけです。
fact step { init[to/first[]] all t : Time - to/last[] | let t' = to/next[t] | one obj : Object | (assign[t, t', obj] or copy[t, t', obj] or execute[t, t', obj] or executeRest[t, t', obj]) and (all obj' : Object | obj' != obj => noChangeAll[t, t', obj']) }
アサートはstateを使って以下のように書きなおします。
assert RealValueEqualsExpectedAfterExecute { all t : Time | all obj : Object | obj.state.t = ExecuteDone => obj.real.t = obj.expected.t } check RealValueEqualsExpectedAfterExecute for 3 but exactly 1 Object, 4 Time, 4 seq
再びアサートをチェックします。
No counterexample found. Assertion may be valid. 16ms.
オブジェクトがひとつの場合だと反例は見つからないようです。では少しスコープを広げてみます。
check RealValueEqualsExpectedAfterExecute for 3 but exactly 2 Object, 4 Time, 4 seq
すると、
Counterexample found. Assertion is invalid. 62ms.
今度は反例が見つかりました。[Show]で確認すると以下のシーケンスで違反があることが分かりました。
- 初期状態は、Object0.real = Object0.expected = Zero, Object1.real = Object1.expected = Two
- Object0.assgin(One). Object0.real = Zero, Object0.expected = One
- Object1.copy(Object0). Object1.real = Two, Object1.expected = One (= Object0.expected )
- Object1.execute. Object1.real = Zero, Object1.expected = One
これは、Object1 のキューに入っている copy を実行する前にコピー元となっている Object0 の assgin を実行していないためです。どうやらget前にexecuteするだけでは不十分なようです。
2番目のモデル ー コピー元をexecuteする ー
上記の反例では、コピー時のコピー元の値が期待値と実値で一致していないことが原因だったので、以下のような、copyコマンドをキューに積むときには、コピー元のオブジェクトをexecuteする実装を考えました。
void copy(Object obj) { obj->execute(); // <- コピー元のexecuteを呼ぶ add_command(make_command(copy, obj)); }
fact を使って copy コマンドをキューに積む前に execute するようにします。こうするとcopyコマンドを積む前にexecuteが実行するパスしか検査しません。
fact ExecuteCopySrc { all t' : Time - to/first[] | let t = to/prev[t'] | all obj : Object | (obj.state.t != CopyAdded && obj.state.t' = CopyAdded) => let command = last[obj.q.t'] { ((command.src).state.t = ExecuteDone or (command.src).state.t = InitDone) } }
では検査してみましょう。
assert RealValueEqualsExpectedAfterExecute { all t : Time | all obj : Object | obj.state.t = ExecuteDone => obj.real.t = obj.expected.t } check RealValueEqualsExpectedAfterExecute for 3 but exactly 2 Object, 5 Time, 5 seq
以下のような反例が見つかりました。
- 初期状態は、Object0.real = Object0.expected = Two, Object1.real = Object1.expected = Zero
- Object1.copy(Object0). Object1.real = Zero, Object1.expected = Two (= Object0.expected )
- Object0.assgin(One). Object0.real = Two, Object0.expected = One
- Object0.execute. Object0.real = One, Object0.expected = One
- Object1.execute. Object1.real = One, Object1.expected = Two
これは、Object1 のキューに入っている copy を実行する前にコピー元となっている Object0 の値が 3, 4 で Object1 が execute する前に変更されたために起きた違反です。
3番目のモデル ー キューイングするのをひとつずつ ー
上記の反例は、キューに入ったコピー元がそのあとで値を変えてしまうために起きていました。そこで、上記の反例のシーケンスの 2 と 3 の間にも execute する方法を考えました。思いついたのは、新しくコマンドをキューに積むときに、最後にコマンドを積んだオブジェクトを異なる場合はキューに積む前に execute したらどうかというものです。コードにすると以下のような実装です。
void assign(int v) { if (last != this) { last->execute(); // <- 最後にキューに積んだオブジェクトのexecuteを呼ぶ } add_command(make_command(assign, v)); last = this; }
そこでこれを fact で表して検証してみました。
pred addCommandAtThatTime [t : Time, obj : Object] { let before = to/prev[t] | #(obj.q.before) < #(obj.q.t) } fact ExecuteDoneBeforeChangingTarget { all disj obj1, obj2 : Object { all disj t1, t2 : Time - to/last[] { (to/lt[t1, t2] and addCommandAtThatTime[t1, obj1] and addCommandAtThatTime[t2, obj2]) implies { obj1.state.t2 = ExecuteDone } } } }
これで検証すると反例は見つかりません。以下のように
check RealValueEqualsExpectedAfterExecute for 3 but exactly 9 Time, 7 seq
スコープを広げて検査してみましたが、反例が見つからないのでこのモデルは正しそうだと言えます。
おわりに
上記を実装して計測したところなかなかのパフォーマンスだったので以上で設計を終えることにしました。まだまだ改善の余地はありますが、必要十分ということで終えることにしました。
さて、どうだったでしょうか。テストでは自分が思いつくケースしかテストすることはできませんが、Alloyならば(限られたスコープですが)網羅的に検査してくれます。これがテストとモデル検査との大きな違いです。
Alloyの威力が少しでも伝えればいいのですが。
※今回使用したモデルはgithubにあります。
https://github.com/masateruk/blog/tree/master/20111020