Version 13.03
「コントラクト」という考え方
「前回は、アルゴリズム関数を作る上での基本的なことを説明しました」
『結局は、エラーが出たときにどうするか、だよね』
「そうなるね。エラーに関して、入力を中心に考えると次のバリエーション
があります」
・正しい入力値
・誤った入力値
・微妙な入力値
『ビミョーって……』
「前回みたいに単に10倍するだけだと〈微妙な入力値〉ってないけど、複
雑なアルゴリズムだと実際に処理してみないとわからないから」
『試してみないとわからないのが微妙な入力値、ね』
「次に出力を中心に考えると次のバリエーションがあります」
・正しい出力値
・誤った出力値
・エラー
・停止
「前回の例で言えば、順に、10倍の数、オーバーフローした数、 1 、
assert() で停止、ってところかな」
『そか、 assert() は戻り値で返るわけじゃないから出力じゃないんだね』
「このふたつの組み合わせを考えます」
『組み合わせ?』
「そう。たとえば、正しい入力値が入力された時に許される出力は、次のよ
うになります」
○正しい入力値>正しい出力値
×正しい入力値>誤った出力値
×正しい入力値>エラー
×正しい入力値>停止
『正しい出力値しかダメなんだ』
「正しい入力値だから当然。言い換えると、〈正しい出力値が絶対に出るの
が正しい入力値〉ってことかな」
『卵が先か鶏が先か……』
「誤った入力値の場合はこう」
○誤った入力値>正しい出力値
○誤った入力値>誤った出力値
○誤った入力値>エラー
○誤った入力値>停止
『……誤った入力値の正しい出力値って、なに……?』
「たとえば前回の例で言うと、関数の仕様で〈 214748364 より大きい数が
入力された場合、出力は 2147483647 になります〉っていう仕様にした場合
とか」
・誤った入力値: 214748364 より大きい数。
・正しい出力値: 2147483647 。
『……つまり、オーバーフローする場合には int の最大値を返す、ってわ
け?』
「関数の仕様をそうすることもできる、ってこと。この場合、仕様通りの結
果だから誤った出力値ではないし、エラーとは判断できないからエラーでも
ないし」
『でもなんか……』
「誤った入力値は、アルゴリズム関数を使う側が〈この入力値は誤ってるか
ら、このときはアルゴリズム関数を使わない〉って選べるわけだから、何を
されても文句は言えない、ってところかな」
『だから正しい出力値も誤った出力値も停止もOKなわけね』
「最後に微妙な入力値」
○微妙な入力値>正しい出力値
×微妙な入力値>誤った出力値
○微妙な入力値>エラー
×微妙な入力値>停止
『微妙な入力値の時って、誤った出力値と停止はダメなんだ』
「微妙な入力値の場合は、実際にそのアルゴリズム関数に渡してみないと正
しいか誤っているのかわからないでしょ」
『それで試しに渡してみて、誤ってたり停止されたら困る、ってことね』
「そういうこと。だから、正しい出力値とエラーしか返さないようにする必
要があります」
『んー、なんか結構、このパターンって曖昧な気がするんだけど』
「そう」
『そう、って……』
「なぜかというと、〈仕様〉がすべてだから」
『あ、さっきの〈誤った入力値>正しい出力値〉の場合みたいに?』
「そう。仕様で」
・Aが入力されたらBを出力する。
「と書かれていたら、それは絶対の権力を持ちます。だから」
・数が入力されたらその10倍の数を出力する。
・ただし、 3120987 が入力されたら 0 を返す。
『な、なにそれ、なんで こんな変な数だけ特別に…… 』
「それでも仕様でこう決まっていたらそれに従わなきゃいけないってこと。
理不尽な出力であっても、そこに書かれているのなら〈正しい出力値〉なん
です」
『……』
「さて。今まで説明してきた、関数の仕様のことを【コントラクト】って言
います」
『コントラクト?』
「英語で contract 、【契約】っていう意味」
『関数の契約書って感じね』
「そういうこと。関数の仕様は一種の契約書。この契約書に基づいて、信用
取引でプログラムは成り立つわけです」
『〈信用〉、って前回も言ってたね』
「ランタイム、 Win32 API 、 MFC 、そして自作したものも含めて、世の中
にはたくさんのライブラリがあります。それらを使うということは、仕様に
基づいた、プログラム同士の【契約】を行うということなんです」
『……でもさ、その仕様って結局ドキュメント頼りなんでしょ?』
「ライブラリの中身が見られないときには特にそうだね。見られたとしても
全部追っていくのは不可能に近いし」
『……その契約、破られたとしたら?』
「そう、そこが一番の問題。コントラクトは仕様書、つまりドキュメントや
コメントによって成り立つわけだけど、それはいわば口約束」
『その通りプログラムが動くかどうかは別だもんね』
「そこでアルゴリズム関数を作る側に立ち返ると、次のふたつをちゃんと守
る必要がある、ということがわかると思います」
・関数仕様で全ての場合を網羅する。
・関数仕様を正しく守る関数を作成する。
『ってゆーかそれは当然じゃないの?』
「火美ちゃんは、前回の10倍する関数を作るとき、オーバーフローのこと
考えた?」
『う”……つまりそれは〈関数仕様で全ての場合を網羅する〉を守ってない
ってことね』
「そういうこと。まずこれが重要。この仕様がちゃんとできてないってこと
は契約書に不備があるってこと」
『それって、使う側が、こんな関数使っちゃいけない、ってことなんじゃな
い?』
『それはある意味正しいね。まぁ逆に言うと、その関数は使われないってこ
となんだけど』
「それじゃ意味ないね……結局ちゃんと仕様を決めないといけないってこと
ね」
『そういうこと。で、その仕様通りの関数を作ります。それがすべて。たと
えば』
・入力:整数1
・出力:整数1 * 10
ただし、整数1が 214748364 より大きい時は未定義
「って書いておけば、最初の」
int TenTimes( int p_i )
{
return p_i * 10;
}
「のままでOK。あ、もちろん負の数は考えてない場合の話だけど」
『未定義って?』
「どうなるかわからない、ってこと」
『確かにね……』
「これはこれでコントラクトは成立しているんだけど、使い勝手は悪いから
少し変えてみます」
・入力:整数1
ただし、 214748364 より大きい値は入力不可
・出力:整数1 * 10
「こうした場合、一応前のままでも問題なし」
『問題なし? 入力不可なんでしょ?』
「入力不可だけど、入力されちゃった時のペナルティについては仕様に書か
れてないから」
『されたら変な数が返されるだけ、ってことね』
「で、逆の視点で、このコントラクトが守られているかチェックすべき、
っていうことも言えます」
#include <assert.h>
int TenTimes_WithAssert( int p_i )
{
assert( p_i <= 214748364 );
return p_i * 10;
}
『それがこの assert() 版ってわけね』
「 assert() はプログラム上〈絶対にあり得ないこと〉をチェックします。
〈入力不可〉の値はその〈あり得ないこと〉だから」
『 assert() でチェックするわけね』
「コントラクトそれだけだとただの口約束だけど、こうして assert() を使
えばそれをプログラムとして成り立たせることができます」
『そか、 assert() がなかったら、〈入力不可〉でも渡せちゃう、でも
assert() があれば止まっちゃうからどうしようもない、と』
「そういうこと。こうすれば、渡す前に必ずチェックするでしょ」
『 assert() が、使う側にそれを強制する、それがコントラクトを守らせる
ってことになるわけか』
「この、コントラクトを〈口約束〉から〈実効能力のある約束〉に置き換え
ることが非常に重要になるんです」
/*
Preview Next Story!
*/
「というわけで、次回も引き続きコントラクトの話です」
『ちょっとコーディングとは違う話だよね』
「そうだね、プログラム全体の考え方かも」
『でもさー、関数って、ただ作って使うだけじゃダメなの?』
「家を建てる時に、耐久試験をしてない鉄材を使って欲しい?」
『う”……確かに』
「というわけで次回」
< Version 13.04 テストプログラム >
『につづく!』
「というわけで次回は耐久試験の仕方についてです」
『プログラムにもあるの?』
「実はあるんです」