2024-12-25
契約プログラミング(乱文です)
関数の引数はそれぞれ何で、どんな性質を期待するかや、そのほかの事前条件を満たした上でこの関数を呼び出すと、その結果としてどんな返り値が得られてそれ以外にどんな副作用があるかを表明するのが関数の契約。文章を用いるのが簡単だし、伝えやすくするには図を用いても良いし、型とかバリデーションのための式で表現しても良い。何にしても契約を表明すると楽しくプログラミングできる。
この関数の呼び出し側はどんなふうにこの関数を使って処理を実現するだろうかとか、この関数を実装するときはどんなふうに作ろうかと考えるのがまず楽しい。みんなが幸せになれる契約をデザインするのが楽しい。ソフトウェアのテストは契約を守っていることを検証することだと思う。型を用いた表現は契約の表明で、型検査が検証(それも厳密な)である。
このように良い契約をデザインするのがプログラミングでまずやることだと思う。その次に自動で検証するためのコードを書くのがTDDの人だし、関数型の怖い人は型で契約を表現しようとするかもしれない。そのためにすごい表現力を持つ型システムを考えて勉強して使う印象がある。
TDDで最初から完成版のテストを書かないことからわかるように、契約を最初から詳細まで書く必要はない。そんな契約を満たす実装を書けるだろうかと思うこともある。なので最初はふんわり「ユーザの情報を返すAPI」とか決めておいて、IDが欲しいとか、シャーディングしてるから作成日時も欲しい、みたいな事前要件を足したり、ユーザ情報全部返すのはきついし呼び出しがわも幸せじゃないから、名前とメルアドだけ返すことにする、みたいな調整も入るだろう。課金が滞っているユーザに対しては普通の結果は返さない、代わりにエラーを返す、みたいな変更も実装したり呼び出しているうちにしたくなるはず。
結局のところ普通のプログラミングをしようと言っているだけなのだが、僕が強調したいのは契約と実装を分けて考えようということ。契約が先にあってそれに実装を合わせる(合ってない状態はバグってる状態)考え方でいたいし、僕と契約を共有する人には(その共有する契約のレイヤで)いて欲しいと思う。
僕はmac osのコードがどうなっているかを気にすることはなくて、普通にパソコンが動けば満足するし、実際動いているので不満はない。キーボード叩いたら文字が出るという契約を守ってくれている。でもosのapiを叩く友達はそもそもドキュメントが公開されてなくて辛い、みたいなことを言っていた。このように、自分が関心のあるレイヤの契約が問題になるのだと思う。
契約はプログラミングの細かいレベルだと変数の名前とか、改行の置き方によって表明されるもので、大きいレベルだとサービスやロールの責務として表現されると思う。サービスのAPIは代表的な契約だと思う。APIという言葉は契約と同じような意味だと思うけど、エンドポイントみたいな意味でも使われているし責務みたいな概念には適用されないと思っていて使わない。
僕たちが欲しいものは何かを表明しよう、というのが契約を表明しようということだと言い換えられる。使う側からしたらその実現方法は関係ないし、提供する側からしたら契約さえ満たせば何をやっても良い。getterとsetterを提供するからと言ってそういうフィールドをよく知られた方法で提供しないといけないわけではないし、バッファリングとか遅延実行をしても良い。
未定義な状況とか意図しない状況はある。今と昔では状況が変わって、その頃は妥当に思えた契約が時代に合わなくなることもある。契約がないと実装が良くないで話が終わってしまうが、契約があれば仮定が違ったのだと結論づけられる。人の記憶があればその人の心に契約があって仮定が違ったと判断できるだろう。記憶を失ったりその人が失われたりする場合は契約を表明しておくべきだろう。
型はデータ構造やその不変条件を表現したりそれらに名前をつけることで、契約を表現する。そして型を用いて表明された契約は普通型検査によって契約が満たされていることを検証する。正しい場合には正しいと判定するし、正しくない場合には正しくないと判定する。さらに、ある程度ちゃんとした型検査器は、どのように契約が満たされなかったかを説明してくれる。
テストは契約を検証するためのコードで、そのテストが契約のどんな部分を検証するか、テストケースの名前として表明される。その表明を満たすようにテストコードが実装されて、テストランナーに呼ばれる。契約のどんな側面をそれぞれのテストケースで検証するか、検証すると幸せか(誰にとって?)を決めるのはテスト実装者のスキルに依存するだろう。契約の中で脆いところを普通は検証するのだろう。明らかに正しそうなやつは手を抜いてしまえるはず。
バリデーションも契約を表現する手段の一つ。契約を検証処理として実装して表明するのがバリデーション。型と同じようにすべての実行について契約が満たされるかを検証する。一方でテストと同じように実際に動くものに対して検証を行う。
実際に動かして試すのは何だろう。契約とはあまり関係なさそう。実装したやつがどんな感じになってるか微妙なので試して動作を観察するのがとりあえず動かしてみる目的。これは脇道だったな。ただし、あまりによくわからなくなっているときは分割統治をするときだと思う。自分の手の中にある対象をコンポーネントに分割して、それぞれに対して契約をデザインする。その契約を満たすように実装しつつ、その契約を期待して組み合わせて元々の目的を達成すれば良い。わからない部分が新しい契約の内側にはいれば小さくなった簡単な契約を満たすことに問題は帰着できたし、それらの契約を組み合わせることに課題があるなら詳細を忘れて便利な道具を手に入れられたのだからやはり問題は簡単になっているはず。うまく契約を設計することが難しいなら、それは楽しいから問題ない。ずっと取り組んでいればいい。
何にしても契約を表明することが何より先にくる。これだけでプログラミングが楽しくなる。その後に型、テスト、バリデーションを書いたり実装をしたりする。
そういう考え方が how to design programsにデザインレシピとして書いてあるのでよかったら読んでみてください。
プログラミング言語はこの辺りが優秀で、型システムの研究とかで契約を表明して保証する方法を検討しているし実際に生かされている。なので言語に守られているうちは割と安心できる。あとは怠慢にならなければいいだけ。
プログラミング言語に閉じられなくなるとき、例えばWeb APIを提供するときは急に難易度が上がる。Web APIでも契約を表明して検証するためにスキーマ駆動開発がある。契約を書き下すことを支援するのはもちろん、型とかバリデーションで表明することも支援する。テストツールの支援もある。
僕たちが求めているのは以下である。
- 契約を表明すること
- 契約を洗練すること
- 契約を犯せないようにすること
- 契約を犯しかけたらなるはやで誰かに教えてほしい
- これらを簡単に実現できること
最初のやつは設計しようぜ!という話。次が設計技法とか良い設計みたいな話。三つ目は型とかバリデーション、テストの話。次もそう。最後のやつはそれら全てを支援するエンジニアリングの話。
プログラミングパラダイム
契約の話をしたのでパラダイムの話をしたい。オブジェクト指向と普通のやつの話。どっちでもいいけど、契約を表明しやすいパラダイムが良いと思う。
オブジェクト指向な考え方だと、内部状態を持つオブジェクトがメソッドを受信してその結果内部状態を変えたり返信したりする。そういうふうに契約を表明して守っていくならオブジェクト指向を使えばいい。
関数型みたいな普通のやつは、仮定が与えられた状況で関数を呼ぶと結果を出す。そういう契約の書き方をするなら関数型みたいに書けばいい。
どちらも相互に変換可能だから力まず表現しやすい言葉遣いを使えば良いだろうと思う。
感想
割と思いの丈をダンプした。これは就職したからできたことのように思う。学生の頃に培った理想が現実世界では必ずしも簡単に実現しないことを体験できた。自明だと思っていた領域の非自明な部分に気がついて思うところが出てきた。
契約を設計する楽しさ
プログラミングの楽しいところは、ロールを分けてうまく契約群を設計することにあると思う。フレームワークを作るのはそういう契約の集まりを定義することだから楽しい。一方でフレームワークを使うと契約を設計する機会を奪われる分楽しくないかもしれない。
その分他のレイヤに注力すればいいのだろうが。
自分が自由に設計したいレイヤで他人に契約を決められるとストレスだろう。本当につまらない。反対に自分が関心なくて誰かにうまくやって欲しいときには良い契約を強い人に決めて欲しいと思う。そういうレイヤを担当するフレームワークを使って欲しいし、技術を選定したい。
アーキテクチャ設計についても言及できそう。アーキテクチャは中長期的には変遷するものだが、短期的には変わらない。アーキテクチャに乗っかってここのコンポーネントを作り込む。ここのコンポーネントをストレスなく作り込みやすくするアーキテクチャが良いアーキテクチャなのだろう。