オブジェクト指向分析(OOA)や構造化分析(SA)を教えていると、受講生は必ずと言ってよいほど、 仕様記述のところでつまずく。これはOOAやSAで仕様記述言語が決っておらず、 「前条件や後条件などを使って、何をするのか、適当に仕様を書け」と教えざるを得ないためである。 もちろん、前条件や後条件の書き方は教えるのだが、すべてを厳密な文法で規定しているわけではないので、 どこかで必ず曖昧さが生じる。ここを適当に日本語を交えながら、 しかもできるだけ曖昧さがないように記述するには、確かにOOAやSA以外に色々な知識がいる。
そこで、仕様記述言語を一応定めて、小さな仕様を書いて実験してみた。
OOAやSAの仕様は「操作仕様」とか「プロセス仕様」とか呼ばれているが、 結局何らかの機能を果たす関数の仕様と解釈できるので、関数型言語MLを仕様記述言語として採用した。 分析段階では「HowではなくWhatを書く」ことが重要なので、手続き的言語でなく、 宣言的プログラミングができることもありがたい。 フリーのML処理系( Standard ML ) がUnixやMacintosh上で動くことも、MLを採用した重要な動機である。
とりあえず、日曜日の夜のやっつけ仕事で片付けられるようにSRAの「出張費の精算」を例題に選んだ。 詳細は、仕様記述と共に説明していく。
さて、MLは漢字が今のところ処理できないのであるが、 仕様記述にはやっぱり『漢字』が使いたい。で、仕様記述は漢字混じり仕様とし、 漢字をローマ字に直してMLのプログラムとし、プロトタイプを作って動かしてみることにする。
まず、出張費の定義は次のようになる。
fun 出張費 (宿泊費, 交通費, 日当, 雑費) = 合計(宿泊費) + 合計(交通費)
+ 合計(日当) + 合計(雑費);
出張費は、宿泊費・交通費・日当・雑費を引数とした関数で、「合計」という下働きをする関数を呼び出している。 合計の定義は次のとおり。
fun 合計[] = 0 | 合計 (n::ns) = n + (合計 ns);
「合計」はリストの各要素を合計する関数である。 まだ分析の段階で「リスト」などという設計や実現に絡む用語が出てくるのは気にくわない。 が、何日間か出張していたとして、その日付分の宿泊費などを「合計する」ことを厳密に書くとすれば、 データの持ち方を決めないわけにもいかない。
「合計」の定義は、リストが空だったら0を返し、整数nに続いてリストnsが来るパターンの時は、 再帰的に「合計」を呼び出し、nにその合計を加える。 このパターンマッチがMLの強力なところである。 (合計 ns) は合計(ns)と書いてもよい。
合計の動きを引数が[1,2,3]の場合について書き下すと、以下のようになる。
MLはデータの型を類推し、決定する。情報が足りなければ文句を言う。 今の場合、「合計」は、合計[] = 0の定義から、 整数のリストを引数とし整数を返す関数であることが分かるので怒られない。 従って,「出張費」関数も整数のリストを四つもらって、整数を返す関数になる。
宿泊費はSRAの場合、肩書と宿泊した都市と領収書の有無を引数とした関数である。定義は次のとおり。
fun 規定宿泊費(肩書, 都市, 領収書) =
if not 領収書 then
宿泊費(肩書) div 2
else
if 大都市(都市) then 宿泊費(肩書) + 1000 else 宿泊費(肩書);
fun 宿泊費(肩書) =
if 肩書 = "主幹" then
9000
else
if 肩書 = "主席" then 8500 else 8000;
fun 大都市(都市) =
let val 政令指定都市 = ["東京","横浜","名古屋","京都","大阪",...]
fun eq c = if 都市 = c then
true
else
false
in exists eq 政令指定都市 end;
普通の言葉で言うと、 関数「規定宿泊費」の定義は、 「領収書が無ければ規定の宿泊費の半分で、宿泊した都市が大都市なら1000円割増しで、それ以外は規定通り」 という曖昧なものになる。 関数「宿泊費」は、肩書を引数として宿泊費を返しているだけである。
関数「 大都市 」は、都市が大都市ならtrueを、そうでなければfalseを返す。 標準ライブラリーの関数existsは、リストを引数(今の場合「政令指定都市」)とする関数 (今の場合eq)自身を引数としてもらい、リストの要素全てにその関数(eq)を適用し、 ひとつでもtrueがあればtrueを返す。
日当の定義は次のようになる。
fun 日当合計(肩書, 出発日時, 到着日時) = let val 日数 = 日数を求める(出発日時,
到着日時) val 出発 = Time(時(出発日時),分(出発日時),秒(出発日時)) val 到着 = Time(時(到着日時),分(到着日時),秒(到着日時))
fun 日当を合計する(日数, 出発, 到着) = case 日数 of 1 => 出発日日当(肩書, 出発) |
2 => 出発日日当(肩書, 出発) + 到着日日当(肩書, 到着) | n => 出発日日当(肩書, 出発)
+ 到着日日当(肩書, 到着) + 日当(肩書) * (n - 2) in 日当を合計する(日数, 出発, 到着) end
fun 出発日日当(肩書, 出発) = let val 出発時 = Time2Day(出発) in if 出発時 >
0.5 then 日当(肩書) div 2 else 日当(肩書) end; fun 到着日日当(肩書, 到着) = let
val 到着時 = Time2Day(到着) in if 到着時 < 0.5 then 日当(肩書) div 2 else
日当(肩書) end;
「日当合計」は肩書と出発日時と到着日時とを引数としてもらい、全出張日の日当の合計を計算する。 まず、あらかじめDateモジュールで定義された「日数を求める」という関数を呼んで、出張の「日数」を計算する。 「日数を求める」はDateモジュールで、正味の日数を計算する「正味日数を求める」関数を使って、 以下のように定義されている。
fun 日数を求める(Datetime(年,月,日,時,分,秒), 日時2) = 正味日数を求める(Datetime(年,月,日,24,0,0),
日時2) + 1;
すなわち、Datetime(年,月,日,時,分,秒)で表される日時から、日時2で表される日時までの日数計算をするのに、 まず最初の日時の真夜中24時(Datetime(年,月,日,24,0,0))から日時2までの日数を計算し、それに1を加える。 こうすると、例えば夜の21時に出張に行き朝の8時に出張から返ってくると、 正味の日数計算では1日ということになってしまうが、「日数を求める」関数は2日を返してくれる。
次に、日数が1か2かそれ以外かで場合分けし、 出発日の日当・到着日の日当を、それ以外の日の日当を計算する関数を使って日当合計を求める。
「出発日日当」と「到着日日当」は、それぞれ出発日と到着日の日当を計算する関数で、 「出発」という引数は出発時刻を表す。普通の言葉で言うと、 「出発が午後か、到着が午前中だったら、日当は半分ね」ということになる。
出張精算の仕様の全体は以下のようになる。 signature 規定SIG = sig val
規定宿泊費 : string * string * bool -> int val 大都市 : string ->
bool val 日当 : string -> int val 宿泊費 : string -> int val
出発日日当 : string * Date.datetime -> int val 到着日日当 : string *
Date.datetime -> int val 日当合計 : string * Date.datetime * Date.datetime
-> int end; structure 規定 : 規定SIG = struct open Date; fun 宿泊費(肩書)
= if 肩書 = "主幹" then 9000 else if 肩書 = "主席"
then 8500 else 8000; fun 大都市(都市) = let val 政令指定都市 = ["東京","横浜","名古屋","京都","大阪",...]
fun eq c = if 都市 = c then true else false in exists eq 政令指定都市
end; fun 規定宿泊費(肩書, 都市, 領収書) = if not 領収書 then 宿泊費(肩書) div 2 else
if 大都市(都市) then 宿泊費(肩書) + 1000 else 宿泊費(肩書);< fun 日当(肩書) =
if 肩書 = "主幹" then 3500 else if 肩書 = "主席"
then 3000 else 2500; fun 出発日日当(肩書, 出発) = let val 出発時 = Time2Day(出発)
in if 出発時 > 0.5 then
日当(肩書) div 2 else 日当(肩書) end; fun 到着日日当(肩書, 到着) = let
val 到着時 = Time2Day(到着) in if 到着時 < 0.5 then 日当(肩書) div 2 else
日当(肩書) end; fun 日当合計(肩書, 出発日時, 到着日時) = let val 日数 = 日数を求める(出発日時,
到着日時) val 出発 = Time(時(出発日時),分(出発日時),秒(出発日時)) val 到着 = Time(時(到着日時),分(到着日時),秒(到着日時))
fun 日当を合計する(日数, 出発, 到着) = case 日数 of 1 => 出発日日当(肩書, 出発) |
2 => 出発日日当(肩書, 出発) + 到着日日当(肩書, 到着) | n => 出発日日当(肩書, 出発)
+ 到着日日当(肩書, 到着) + 日当(肩書) * (n - 2) in 日当を合計する(日数, 出発, 到着) end
fun 合計[] = 0 | 合計 (n::ns) = n + (合計 ns); fun 出張費 (宿泊費, 交通費, 日当,
雑費) = 合計(宿泊費) + 合計(交通費) + 合計(日当) + 合計(雑費);
signature 「規定SIG」は、後の方で定義されているモジュール定義のためのstructure 「規定」の型検査の情報からなり、 型や値を定義する。今の場合、 例えば「規定宿泊費」は、1番目と2番目の引数が文字列、3番目の引数が論理値で、 整数が返ってくる値として定義されている。
structure 「規定」は、規定に関する定義がひとまとめにされたモジュールである。
上の仕様を、ほとんどローマ字に直しただけのものが、以下のMLプログラムである。 (*で始まり、*)で終わる部分は注釈であり、そこに各関数の呼び出し例が書いてある。
結果は、>の後に示す。 signature KITEI = sig val kiteiSyukuhakuhi
: string * string * bool -> int val daitosi : string ->
bool val nittou : string -> int val syukuhakuhi : string ->
int val syuppatubiNittou : string * Date.datetime -> int val
toutyakubiNittou : string * Date.datetime -> int val nittouGoukei
: string * Date.datetime * Date.datetime -> int end; structure
Kitei : KITEI = struct open Date; fun syukuhakuhi(katagaki) =
if katagaki = "syukan" then 9000 else if katagaki =
"syuseki" then 8500 else 8000; fun daitosi(tosi) =
let val largeCity = ["sapporo","sendai","tokyo","yokohama","nagoya","kyoto","osaka","kitakyusyu"]
fun eq c = if tosi = c then true else false in exists eq largeCity
end; fun kiteiSyukuhakuhi(katagaki, tosi, ryousyusyo) = if not
ryousyusyo then syukuhakuhi(katagaki) div 2 else if daitosi(tosi)
then syukuhakuhi(katagaki) + 1000 else syukuhakuhi(katagaki);
fun nittou(katagaki) = if katagaki = "syukan" then
3500 else if katagaki = "syuseki" then 3000 else 2500;
fun syuppatubiNittou(katagaki, syuppatu) = let val syuppatuji
= Time2Day(syuppatu) in if syuppatuji > 0.5 then nittou(katagaki)
div 2 else nittou(katagaki) end; fun toutyakubiNittou(katagaki,
toutyaku) = let val toutyakuji = Time2Day(toutyaku) in if toutyakuji
< 0.5 then nittou(katagaki) div 2 else nittou(katagaki) end;
fun nittouGoukei(katagaki, syuppatunitiji, toutyakunitiji) =
let val nissu = NumberOfDays(syuppatunitiji, toutyakunitiji)
val syuppatu = Time(Hour(syuppatunitiji),Minute(syuppatunitiji),Second(syuppatunitiji))
val toutyaku = Time(Hour(toutyakunitiji),Minute(toutyakunitiji),Second(toutyakunitiji))
fun acc(nissu, syuppatu, toutyaku) = case nissu of 1 => syuppatubiNittou(katagaki,
syuppatu) | 2 => syuppatubiNittou(katagaki, syuppatu) + toutyakubiNittou(katagaki,
toutyaku) | n => syuppatubiNittou(katagaki, syuppatu) + toutyakubiNittou(katagaki,
toutyaku) + nittou(katagaki) * (n - 2) in acc(nissu, syuppatu,
toutyaku) end; end; (* syuppatubiNittou("syukan", Time(12,0,0));
syuppatubiNittou("syukan", Time(11,59,0)); syuppatubiNittou("syukan",Time(12,1,0));
syuppatubiNittou("syuseki", Time(11,59,0)); syuppatubiNittou("syuseki",
Time(12,1,0)); syuppatubiNittou("syain", Time(11,59,0));
syuppatubiNittou("syain", Time(12,1,0)); toutyakubiNittou("syukan",
Time(11,59,0)); toutyakubiNittou("syukan", Time(12,1,0));
toutyakubiNittou("syuseki", Time(11,59,0)); toutyakubiNittou("syuseki",
Time(12,1,0)); toutyakubiNittou("syain", Time(11,59,0));
toutyakubiNittou("syain", Time(12,1,0)); syukuhakuhi("syukan");
kiteiSyukuhakuhi("syukan", "yokohama", true);
kiteiSyukuhakuhi("syukan", "kyoto", true);
kiteiSyukuhakuhi("syukan", "nagano", true);
[kiteiSyukuhakuhi("syukan","yokohama",true),
kiteiSyukuhakuhi("syukan","nagano",true),
kiteiSyukuhakuhi("syukan","yokohama",false)];
nittou("syukan"); nittou("syuseki"); nittou("syain");
nittouGoukei("syukan", Datetime(1993,7,5,12,0,1), Datetime(1993,7,10,12,0,0));
should be 19250 *) fun sum[] = 0 | sum (n::ns) = n + (sum ns);
(* - sum([1,2,3]); val it = 6 : int *) fun syuttyouhi (s, k,
n, z) = sum(s) + sum(k) + sum(n) + sum(z); (* val syuttyouhi
= fn : int list * int list * int list * int list -> int *)
(* syuttyouhi([10000,1000,11000], [12000,400,12000], [3500,3500,3500],
[120,240,100]); syuttyouhi([kiteiSyukuhakuhi("syukan","yokohama",true),
kiteiSyukuhakuhi("syukan","nagano",true),
kiteiSyukuhakuhi("syukan","yokohama",false)],
[10000,2000,11000], [nittouGoukei("syukan",Datetime(1993,2,28,11,59,0),Datetime(1993,3,2,11,59,0))],
[120,240,0]); should be 55610 syuttyouhi([kiteiSyukuhakuhi("syukan","yokohama",true),
kiteiSyukuhakuhi("syukan","nagano",true),
kiteiSyukuhakuhi("syukan","yokohama",false)],
[10000,2000,11000], [nittouGoukei("syukan",Datetime(1993,2,28,12,1,0),Datetime(1993,3,2,11,59,0))],
[120,240,0]); should be 53860 syuttyouhi([kiteiSyukuhakuhi("syukan","kanazawa",true)
* 4, kiteiSyukuhakuhi("syukan","kanazawa",false)],
[150,300,14600,1000,1700*4,850,1000,14600,300,150], [nittouGoukei("syukan",Datetime(1993,7,5,9,0,0),Datetime(1993,7,10,13,0,0))],
[]); should be 101250 *)
出張費の精算といった事務処理問題の分析モデルの仕様記述には、 関数型言語MLをベースとした仕様記述はとりあえず使えそうである。 例題中に示した日本語の「曖昧な仕様」よりは随分とましであろう。 ただ、分析モデルの仕様記述でありながら、リスト構造といったデータ構造を決めなければならないあたりが、 やむを得ないとはいえ煩わしい。そのかわり、書いた仕様が実行可能であるという御利益がある。
ここでは述べなかったが、MLは、functorというstructureからstructureへのマッピングと、 signatureとstructureとを用いて、オブジェクト指向の「継承」をうまく表すこともできる。 従って,SAだけでなくOOAの仕様記述にも向いている。 また、MLは手続き型プログラミングもできるので、構造化設計やオブジェクト指向設計といった、 Howを記述する必要がある仕様記述言語としても流用できる。
半面、例題にも必要だったが、日付モジュールやその他の事務処理に必要なライブラリーが揃っていないため、 すぐにこの分野に適用するのは難しい。
なお、この文章とプログラムは4時間ほどで作り、MLの経験自体も教科書を読みはじめてから4カ月程なので、 考慮が足りない面もあるだろうがお許しいただきたい。
ここで使用した Standard ML of New Jersey の処理系は、以下のインターネットのFTPサイトから入手できる。
princeton.edu(128.112.128.1) research.att.com(192.20.225.2)
ログイン名はanonymousで、あなたのメールアドレスをパスワードに入れる。 ディレクトリーはpub/ml (princeton.eduの場合) またはdist/ml (research.att.comの場合)である。 ftpはbinary mode ("binary")にする。 これ以上の情報は、READMEとrelease-notes.psファイルを参照して欲しい。