契約プログラミング

契約プログラミングは、ソフトウェアの一部を、互いにサービスを提供する個別のエンティティとして扱うソフトウェア設計手法だ。この手法では、サービスの提供者と利用者が契約に従っている限り、ソフトウェアは仕様どおりに動作することが実現される。

Dの契約プログラミング機能では、ソフトウェアサービスの単位として関数が使用される。ユニットテストと同様に、契約プログラミングもassertチェックに基づいている。

Dの契約プログラミングは、3種類のコードブロックによって実装されている。

invariantブロックと契約継承については、構造体とクラスについて説明した後、後の章で説明する。

in事前条件用のブロック

関数の正しい実行は、通常、そのパラメータの値が有効であるかどうかによって決まる。例えば、平方根関数は、そのパラメータが負ではないことを要求する場合がある。日付を扱う関数は、月の番号が1から12までの間であることを要求する場合がある。このような関数の要件は、その関数の前提条件と呼ばれる。

このような条件チェックは、assertおよびenforceの章で既に紹介した。パラメータ値の条件は、関数定義内のassertチェックによって強制することができる。

string timeToString(int hour, int minute) {
    assert((hour >= 0) && (hour <= 23));
    assert((minute >= 0) && (minute <= 59));

    return format("%02s:%02s", hour, minute);
}
D

契約プログラミングでは、同じチェックが関数のinブロック内に記述される。inまたはoutブロックを使用する場合、関数の実際の本体はdoブロックとして指定する必要がある。

import std.stdio;
import std.string;

string timeToString(int hour, int minute)
in {
    assert((hour >= 0) && (hour <= 23));
    assert((minute >= 0) && (minute <= 59));

} do {
    return format("%02s:%02s", hour, minute);
}

void main() {
    writeln(timeToString(12, 34));
}
D
contracts.1

注釈:以前のバージョンのDでは、doの代わりにbodyキーワードがこの目的で使用されていた。

inブロックの利点は、すべての前提条件をまとめて、関数の実際の本体から分離できることだ。こうすることで、関数本体には前提条件に関するassertチェックが不要になる。必要に応じて、関数本体内に、関数本体内の潜在的なプログラミングエラーを防ぐための、関連のないチェックとして、他のassertチェックを挿入することは、依然として可能であり、お勧めだ。

inブロック内のコードは、関数が呼び出されるたびに自動的に実行される。関数の実際の実行は、inブロック内のすべてのassertチェックが合格した場合にのみ開始される。これにより、無効な前提条件で関数が実行されるのを防ぎ、その結果、誤った結果が生成されるのを回避できる。

inブロック内でassertチェックが失敗した場合は、呼び出し元によって契約が違反されたことを示している。

out後置条件用のブロック

契約のもう一方は、関数が提供する保証である。このような保証は、関数の事後条件と呼ばれる。事後条件を持つ関数の例としては、2月の日数を返す関数がある。この関数は、返される値が常に28または29であることを保証できる。

後条件は、関数のoutブロック内でチェックされる。

return文によって関数が返す値は、関数内で変数として定義する必要がないため、通常、戻り値を参照する名前はない。これは、assertoutブロック内でチェックする際に、返された変数を名前で参照できないため、問題となる場合がある。

Dは、outキーワードの直後に戻り値の名前を指定する方法を提供することで、この問題を解決している。この名前は、関数が返している値そのものを表す。

int daysInFebruary(int year)
out (result) {
    assert((result == 28) || (result == 29));

} do {
    return isLeapYear(year) ? 29 : 28;
}
D

resultは返される値として妥当な名前だが、他の有効な名前も使用できる。

一部の関数には戻り値がないか、戻り値をチェックする必要がない。その場合、outブロックは名前を指定しない。

out {
    // ...
}
D

inブロックと同様に、outブロックは、関数の本体が実行された後に自動的に実行される。

outブロック内でassertチェックが失敗した場合は、関数が契約に違反したことを示す。

これまで説明してきたように、inブロックとoutブロックはオプションである。同じくオプションであるunittestブロックも考慮すると、D関数は最大4つのコードブロックで構成される。

これらすべてのブロックを使用した例を以下に示す。

import std.stdio;

/* 2つの変数に合計を分配する。
 *
 * 最初に変数1に分配するが、
 * その変数に7以上を与えることはない。
 * 残りの合計は変数2に分配される。 */
void distribute(int sum, out int first, out int second)
in {
    assert(sum >= 0, "sum cannot be negative");

} out {
    assert(sum == (first + second));

} do {
    first = (sum >= 7) ? 7 : sum;
    second = sum - first;
}

unittest {
    int first;
    int second;

    // 合計が0の場合は、両方が0でなければならない
    distribute(0, first, second);
    assert(first == 0);
    assert(second == 0);

    // 合計が7未満の場合は、すべてを
    // 最初の変数に割り当てる
    distribute(3, first, second);
    assert(first == 3);
    assert(second == 0);

    // 境界条件をテストする
    distribute(7, first, second);
    assert(first == 7);
    assert(second == 0);

    // 合計が7以上の場合は、最初の変数に7を割り当て
    // 残りを2番目の変数に割り当てる
    distribute(8, first, second);
    assert(first == 7);
    assert(second == 1);

    // ランダムな大きな値
    distribute(1_000_007, first, second);
    assert(first == 7);
    assert(second == 1_000_000);
}

void main() {
    int first;
    int second;

    distribute(123, first, second);
    writeln("first: ", first, " second: ", second);
}
D
contracts.2

以下のコマンドでプログラムをコンパイルし、ターミナルで実行できる:

dmd deneme.d -w -unittest
./deneme
最初: 7 2番目: 116
Bash

関数の実際の作業は2行だけで構成されているが、その機能をサポートするために、合計19行の重要な行がある。このような短い関数にこれほど多くの余分なコードは多すぎるという意見もあるかもしれない。しかし、バグは決して意図的なものではない。プログラマは、正しく動作することが期待されるコードを常に記述するが、そのコードにはさまざまな種類のバグが含まれてしまうことがよくある。

ユニットテストや契約によって期待が明示的に定義されていると、当初から正しい関数は、その正しさを維持できる可能性が高くなる。プログラムの正確性を高める機能は、すべて最大限に活用することをお勧めする。ユニットテストも契約も、その目標を達成するための効果的なツールだ。これらは、デバッグに費やす時間を削減し、実際にコードを書く時間を効果的に増やすのに役立つ。

式ベースの契約

inおよびoutブロックは、任意のDコードを許可するのに便利だが、通常、前提条件および後条件チェックは単純なassert式にすぎない。このような場合に便宜上、より短い式ベースの契約構文が用意されている。次の関数を考えてみよう。

int func(int a, int b)
in {
    assert(a >= 7, "a cannot be less than 7");
    assert(b < 10);

} out (result) {
    assert(result > 1000);

} do {
    // ...
}
D

式ベースの契約では、中括弧、明示的なassert呼び出し、およびdoキーワードが不要になる。

int func(int a, int b)
in (a >= 7, "a cannot be less than 7")
in (b < 10)
out (result; result > 1000) {
    // ...
}
D

out契約では、関数の戻り値がセミコロン前に指定されていることに注意。戻り値がない場合や、out契約が戻り値を参照していない場合でも、セミコロンは必ず指定する必要がある。

out (; /* ... */)
D
契約プログラミングを無効にする

ユニットテストとは対照的に、契約プログラミング機能はデフォルトで有効になっている。‑releaseコンパイラスイッチは、契約プログラミングを無効にする。

dmd deneme.d -w -release
Bash

‑releaseスイッチでコンパイルされたプログラムでは、inout、およびinvariantブロックの内容は無視される。

inブロックとenforceチェック

assertおよびenforceの章で、assertチェックとenforceチェックのどちらを使用すべきかを判断するのが難しい場合があることを説明した。同様に、inブロック内でassertチェックを使用すべきか、関数本体内でenforceチェックを使用すべきかを判断するのが難しい場合もある。

契約プログラミングを無効にできるということは、契約プログラミングはプログラマーのエラーから保護するためのものだということを示している。そのため、この決定は、assertおよびenforceの章で見たのと同じガイドラインに基づいて行うべきだ。

演習

サッカーの試合の結果に応じて、2つのチームの合計得点を増加させるプログラムを書け。

この関数の最初の2つのパラメータは、各チームが獲得した得点だ。他の2つのパラメータは、試合前の各チームの得点である。この関数は、各チームが獲得した得点に応じて、チームの得点を調整する必要がある。念のため、勝者は3ポイント、敗者は0ポイントを獲得する。引き分けの場合は、両チームに1ポイントずつが与えられる。

さらに、この関数は、どちらのチームが勝ったかを 1(最初のチームが勝った場合)、2(2番目のチームが勝った場合)、0(試合が引き分けた場合)で示す必要がある。

次のプログラムから始めて、関数の4つのブロックを適切に記入しよう。main()内のassertのチェックは削除しないで。この関数の動作を示すためだ。

int addPoints(int goals1, int goals2,
              ref int points1, ref int points2)
in {
    // ...

} out (result) {
    // ...

} do {
    int winner;

    // ...

    return winner;
}

unittest {
    // ...
}

void main() {
    int points1 = 10;
    int points2 = 7;
    int winner;

    winner = addPoints(3, 1, points1, points2);
    assert(points1 == 13);
    assert(points2 == 7);
    assert(winner == 1);

    winner = addPoints(2, 2, points1, points2);
    assert(points1 == 14);
    assert(points2 == 8);
    assert(winner == 0);
}
D
contracts.4

ここではintを返すようにしたが、この関数からはenumの値を返すほうがよいだろう。

enum GameResult {
    draw, firstWon, secondWon
}

GameResult addPoints(int goals1, int goals2,
                     ref int points1, ref int points2)
// ...
D