Slide 1

Slide 1 text

ぼくのかんがえたさいきょう の契約によるプログラミング [email protected] これを酒匂さんの前で話すのすごいぞ!

Slide 2

Slide 2 text

今日の話2 契約によるプログラミングの紹介 20世紀に書いたときの自慢(めっちゃバグ少ないよ) Unit Testingへ 2

Slide 3

Slide 3 text

歴史(自分にとっての) 1980 - 構造体を発明して喜んでいたころ 1988 - ADT, OOP 1990 - オブジェクト指向入門!!! 1999 - Ruby Workshop ???? - TDD ???? - 形式手法のなかま ???? は忘れてしまったの意 3

Slide 4

Slide 4 text

情報が非常に少ない80年代 雑誌記事くらいしかなかった ダンプリストを打ち込む日々 構造体を再発明していた 同じ属性を集めて配列(添え字がオブジェクトの識別 子)にするBASIC的なプログラミングよりも、オブ ジェクトごとにデータをまとめて配置するほうが書きや すいぞ! のちに構造体という概念を知る 4

Slide 5

Slide 5 text

ADTとOOP (88-89年) 抽象データ型の授業があっておもしろかった! テキストはModula-2だったが処理系はなかった THINK Pascal(Object Pascal)で実験 英語のテキストを先生が和訳してくれていた(と思う) 5

Slide 6

Slide 6 text

オブジェクト指向入門!!! めっちゃ読んだ! 全然入門書ではない Eiffelという言語の神話(試せないんだもん) 型を使うっていう意味を知る いろいろすごい本だが表明を使ったプログラミングが めっちゃすごい 契約によるプログラミング 初版の著者名が誤っている(酒しか合っていない) 6

Slide 7

Slide 7 text

表明(assertion) 実行可能なコードにその目的を表したものを付加 実現方法とは無関係にその要素が何をしなければならな いのかを記述する たしかにCのassert()はそうなってるなー 7

Slide 8

Slide 8 text

RubyWorkshop(1999) 日本オラクルでやってた https://www.jus.or.jp/workshop/ruby/ruby.html encoding: euc-jp Rubyのrequire, ensureはEiffelから、みたいなこと を言ってたよ! ただし意味は全く違う Matzもオブジェクト指向入門を読んでいた!! 8

Slide 9

Slide 9 text

契約によるプログラミング クラス(モジュール)と顧客(アプリケーション)の関係は それぞれの権利と義務を表した契約と考える pre の条件を満たした状態で r を呼び出すことを約束 してくださるならば、お返しに私は post の条件を満た す状態を最終的に実現することをお約束します (p.159) エラーチェックを誰が行うべきか明確にする 今世紀では「契約による設計」"DbC"ということが多いかも 9 pre-condition 事前条件

Slide 10

Slide 10 text

事前条件が満たされないとき r (ルーチン・メソッド)はなにをしてもよい クラッシュするかもしれないし、無限ループかもしれ ないし、デタラメな値を返すかもしれない 満たされない場合に備えたコードを書かないってこと 10

Slide 11

Slide 11 text

冗長な検査はよくないよ 渡されたデータが正しい処理のための条件を満たしてい るかどうか、絶えず検査することになる → 複雑さの原 因 ルーチンの中でチェックする?それとも呼び出し側? モジュールの責任分担を決めないと、全くチェックされ ないか、念のため何度もチェックすることになる 冗長な検査がシステムの中に散らばってしまうと、簡素 さが失われ、拡張性、わかりやすさ、保守性も失われる 冗長な検査は複雑さを招くし遅くなるしわかりにくく!技術的負債だよ 11 この方が好ましいと する派閥もある

Slide 12

Slide 12 text

組織的に事前条件を使おう 事前条件が満たされているものとしてルーチンを書ける コードは簡素化され、読みやすさ、保守性があがるぞ プログラミングスタイルが変わると思うからやってみて 呼び出し時の表明の違反を検出できる おかしな引数を渡すことはほぼなくなる そういうテストも不要 12

Slide 13

Slide 13 text

Eiffelによる支援 表明の例 引数の型の情報と表明が両方そろってインターフェイスである 13 class STACK export ... push(x: T) is require not full do ... ensure not empty; top = x; nb_elements = old nb_elements + 1 end; require = 事前条件 fullのときは呼べないよ ensure = 事後条件 emptyではないよ 一番上の要素はxだよ 要素数は旧要素数よりも1増えるよ oldで変更前の値を参照できるぞ oldのほかにnochangeもあるらしい

Slide 14

Slide 14 text

定義域と値域 事前条件は関数の定義域、事後条件は値域 関数を合成するときに値域と定義域がマッチしていなく てはらないぞ! 定義域に相当するクラスを作る (利用者は引数の型と表明を注意深く見る) 14 foo(bar(x))

Slide 15

Slide 15 text

定義域と値域と継承 互換性をもって拡張するとは(つまり継承とか) 事前条件をより弱く、事後条件をより強く 定義域をより広く、値域はより狭く 15

Slide 16

Slide 16 text

Cでやってみた 契約によるプログラミングをCでやろう 事前条件だけでも効果あるな assert()使えば記述できそうだな 定義域に相当するクラスを使おう 値域をできるだけ狭くしよう プログラミングでやるぞ 90年代前半の試み。GUI/Network/重い並行処理とかあるようなシステムでやったよ 16

Slide 17

Slide 17 text

assert()で記述できそうだ 真となる式を書いて表明 偽だと式を印字してクラッシュしてくれる コンパイルオプションで無効にできる assert()を満たせないのはプログラムの誤りなので続行しちゃだめ 17 void MyStackPush(MyStack_p self, Element_p element) { assert(self); assert(element); assert(! myStackIsFull(self)); ... }

Slide 18

Slide 18 text

定義域に相当するクラス 検査済みという情報をオブジェクトの存在で表現する そのオブジェクトが存在するならvalidation済み validならURLオブジェクト、invalidならNULL UIや通信など外からやってくる文字列や数値をそのまま扱わない 18 URL_p URLCreate(const char *str) { ... assert(str); if (! urlParse(str, &desc)) return NULL; ... return self; }

Slide 19

Slide 19 text

不透明な型の表現 ADTをopaqueな型で書く インターフェイスに実装を載せない うっかり自分で生成したりしないですむ C++(を自然に使う)よりも実装を隠せてよい 19 struct App_s; typedef struct App_s * App_p; App_p AppCreate(Bar_p bar, Baz_p baz); void AppForget(App_p app); https://www.jpcert.or.jp/sc-rules/c-dcl12-c.html Create / ForgetはEiffelを 真似した

Slide 20

Slide 20 text

値域を狭くする 呼び出し結果を適切に扱うのは呼び出し側の責任 例:ときどきエラーを返します! エラーをハンドリングするのはアプリ側...しんどい 実行時エラーを極小にする 事前条件を工夫して呼び出し前に気づけるようにとか malloc()のエラーなんて現実的には回避策がないので 単にクラッシュするのはあきらめる ありえない領域を要求するのもバグの一種だぞ どうせならvoidが好き 20

Slide 21

Slide 21 text

プログラミング 設計!というよりプログラミング中にやったよ 一行書くたびに契約について考える OOPは好きだけどOODはよくわからない 21

Slide 22

Slide 22 text

利用者側から書く だいたい利用者側から書く 使う側はどんなオブジェクトがあったら楽かな 操作から必要な型を考える 22 ... StrList_p list; list = StrListCreate(); for (i = 1; i < argc; i++) { if (! argv[i]) break; StrListPush(list, argv[i]); } s = StrListJoin(list, ",");

Slide 23

Slide 23 text

クラス側もほぼ同時に書く インターフェイスと同時にassertを書く 実装はそのあと(意識はするけど) 絶えずコンパイルを通しながら書く malloc()のエラーはとりあえず考えないことが多かった 23 void StrListPush(StrList_p self, const char * str) { assert(self); assert(str); /* ͋ͱͰ */ }

Slide 24

Slide 24 text

やっと中身を実装する 書くの楽 自分の事前条件は満たされてる 自分が誰かを呼ぶ時の事前条件を満たすことに集中 この辺でやっと「実行して試す」状況になる インターフェイスにまつわる問題のバグはないから、動 作そのものを確かめる いわゆるUnitTestはやらなくて、小さなアプリケー ションでテストしてた 24

Slide 25

Slide 25 text

結果 めちゃくちゃコードが短くなった バグ少ない せきのコードのバグが異常に少ないのはなぜか問題 契約に導かれたプログラミングはとてもよい Cの表明の表現力が貧弱だとしても、表明を読み書き しながらプログラミングするのは効果があった 21世紀も一部で引き継がれている 何割かはMFCというかC++に汚染されてしまった 25

Slide 26

Slide 26 text

自慢はここまでだ TDD テスト駆動開発 UnitTest TDDでなくても使う言葉? ここでは使い分けるのめんどくさいからUnitTestって 呼びます 26

Slide 27

Slide 27 text

事前条件・事後条件を見てると UnitTestのtest caseを連想する UnitTestは表明のようなロジックではなく、例示(値の組)で表現されてる気がする 27 class STACK export ... push(x: T) is require not full do ... ensure not empty; top = x; nb_elements = old nb_elements + 1 end;

Slide 28

Slide 28 text

表明とUnitTest Eiffelの表明は実行時検査できる つまり実行しないとダメ UnitTestのtest caseって表明の部分を切り出して実 行するようなモノだろうか 28

Slide 29

Slide 29 text

表明とUnitTest インターフェイスって比較的安定してるからその領域の テストなら頻繁に試す必要あるのかな... 自分たちのプロダクツでUnitTestは非常に少なくアプ リケーション全体の振る舞いのテストが大半を占めるの を思い出した 29

Slide 30

Slide 30 text

表明とモデル検査 test caseで例示できる値の組みは表明が表現してる集 合のごく一部 ほんとに大丈夫なのか? 形式手法のモデル検査みたいな方がよいのでは? こっちの話題は疎いので自信ない 30

Slide 31

Slide 31 text

プログラミングスタイルとして テストに導かれてプログラミングする 表明に導かれてプログラミングする どちらも似てるけど表明の方がわかりやすい印象でっす 型でプログラミングするってこういうことだと思う IDEの補完のためじゃないだろ 31