契約プログラミングをやってみよう!
https://4.bp.blogspot.com/-brqkrQ7s7IA/VxC3SdJ169I/AAAAAAAA52M/Li1_oKQFQqEKcZtRugGBj1Ul89WfIUOEACLcB/s800/keiyaku_contract.png
契約プログラミングってなぁに?
本格的にやったことないので間違ってたら本当に申し訳ないと思う。
契約プログラミングは契約による設計とも呼ばれ "Design by Contracts" (DbC) と呼ばれることもある。 Eiffel というプログラミング言語で初めて導入され現在では D言語 でも利用可能な概念。 契約プログラミングを行う際は、コードの実装やコードの使用方法をドキュメントではなくコードとして取り込み、実装時における「契約」として機能させる。契約に違反するコードが実行された場合、例外として処理され実行が中断されるかあるいはコンパイル時にエラーになる。
契約プログラミングをサポートする言語ではコンパイル時に契約に関する実装自体が除去されるので実行時の効率に影響しないようになる。
契約の条件の種類
契約は、コードの利用条件が満たされることによって成立する。タイミングによって、以下の3種類のようなものがある。
事前条件 (precondition)
引数の型はもちろんだが、具体的に N以上 とか N以下、あるいはXを含む文字列みたいな条件をつけられる。
事後条件 (postcondition)
返り値が具体的にどんな内容であるかの条件をつけられる。
不変条件 (invariant)
コードが実行されている間、オブジェクトやクラスが持っている値の条件をつける。
コードを使う側が、事前条件と不変条件を満たすことで、呼ばれた側のコードは条件が常に満たされていることを前提にコードを書ける。逆に、コードを読んだ側は戻ってくる値が事後条件を必ず満たしていることを前提にコードを書けるメリットがそれぞれある。実行時点で契約の違反があれば例外として処理される。
雑には関数の入力値やオブジェクトのメンバーの値のバリデーションをものすごく手厚くした設計手法と考えることもできそうだ。何より間違いが発生するケースが減るのでテストコードをより限定的により強固に集中して書くようなこともできるようになる。そして先にも記載したように、値のバリデーションを行なっている実装はコンパイル時に除去される。
D言語での契約プログラミング
D言語は公式のドキュメントがしっかりしており、契約プログラミングをサポートしている手前、先の事前条件、事後条件、不変条件についてのサンプルもしっかり記載されている。概略としてはそちらを参考にするとイメージがつきやすいはずだ。
契約プログラミングの雰囲気を掴んでみよう
Proposal に基づきサンプルを示す。下記のコードは実際には動作しないが契約プログラミングをやろうとしたときの雰囲気がわかってもらえれば良い。assertは条件が false になると例外を送出する機構と読み替えて欲しい。
code:dbc-func.ts
/** 正の値を渡すと負値に直して返却する関数 */
function example(arg: number): number {
in {
// 引数は必ず 0 より大きい値であること (事前条件)
assert(arg > 0)
}
out {
// 戻り値は必ず 0 より小さい値であること (事後条件)
assert(arg < 0)
}
body {
// 実際の処理 (引数に -1 を掛けて return)
return arg * -1
// もし実際の処理がこのようになっていれば事後条件を満たさないので例外になる
// return arg
}
}
// 実行可能で -255 が return される
example(255)
// 引数が 0 より大きくない: 事前条件を満たさないので例外が発生する
example(-1)
クラスの場合はさらに不変条件をつけられる。
code:dbc-class.ts
class Example {
public n: number
invariant {
// n は 0 以下になれない
assert(this.n > 0)
}
constructor(arg: number) {
this.n = arg
}
}
// x.n が 0 より大きい値なので実行できる
const x = new Example(255)
// y.n が 0 より小さい値なので例外が発生する
const y = new Example(-1)
イメージ的にはこのような感じになる。TypeScript の型システムに加えて、実行時の状況を言語仕様的に縛ってより固い設計が行え、迷いの起こりにくいコードになっていることがわかる。契約プログラミングにおける条件が満たされていなければ、例外やコンパイルができないため、コードを書く際に何か間違いが起こっていれば、そのコードを書いた人は間違いにすぐ気づけるというわけ。
TypeScript で契約プログラミングを始めてみたい!
先ほどのコードはあくまで雰囲気を掴むためのメタコードなので実際に動作しない。
ではいまある手段で契約プログラミングを実践するためにはどうすればいいだろうか?
実は TypeScript の interfaceを使ったり、戻り値の型を明示するなど、型システムを活用すれば事前条件と事後条件を縛る契約のようなものとして使用できる。手始めとしては可能な限り any を排除し、戻り値の型を明示することを ESLint でチェックするなどして導入していくと良いと思われる。一方で、不変条件や、より細かな事前条件や事後条件を設定するにはこれらだけでは難しい。 そうした機能は現時点ではTypeScriptの言語仕様に取り込まれていないため、より強固に運用を行うには現時点では Node.js で用意されている、assert 関数を使用する。Node.js の組み込み関数なので通常はブラウザでは使用できないが、Webpack や Parcel を使用する、もしくは個別に assert を CDN から読み込むことができればブラウザでも assert 関数は使用可能になる。
サンプルとしては下記のようなコードになる。愚直ではあるが事前条件と事後条件で値の範囲を縛ることができた。
code:pesudo-dbc.ts
import assert from 'assert'
function example(arg: number): number {
// 事前条件: arg は 0 より大きい
assert(arg > 0)
// 実際の処理を行う部分
const result = arg * -1
// 事後条件: result は 0 より小さい
assert(result < 0)
return result
}
example(1) // 戻り値は -1
example(-1) // 事前条件を満たさないため例外
(ちなみに assert を observable な変数の処理に組み込めば 不変条件的な扱いもできなくはないかもしれないがここでは置くこととする。)
そして言及しておかないといけないのは契約プログラミングのポイントは契約の条件に関わる実装は、本番環境あるいはコンパイル後の資産において完全に取り除かれ、アプリケーションの実行に影響を与えないこと。しかしながら先ほどの例は assert が実行時にも残るので、実践的な契約プログラミングとしては不十分になる。
Webpack や Babel のプラグインとして提供されており、トランスパイルやコンパイルの際に assert に関わるコードを除去できる仕組みを提供しているので、契約プログラミングを実践的に行う場合にはぜひ活用したい。(タイミングがあったら別の記事に書こうと思います。)
まとめ
軽く契約プログラミングとはどういうものなのかを紹介しました。
また JavaScript/TypeScript において、assert関数を活用し値の検証を行いつつ、完成した資産にはassertに関わる仕組みを除去することで、いまある仕組みを活用した契約プログラミングを行う手段についてもご紹介しました。 TypeScriptでも言語仕様で契約プログラミングを行えるような提案がされており、既存の型システムと組み合わせて、より強固なアプリケーション開発が行える日がくることが楽しみですね。
ではでは。