$30 off During Our Annual Pro Sale. View Details »

ぼくのかんがえたさいきょうのけいやくによるプログラミング

 ぼくのかんがえたさいきょうのけいやくによるプログラミング

toRuby拡大版 (2023-08-05)

seki at druby.org

August 05, 2023
Tweet

More Decks by seki at druby.org

Other Decks in Programming

Transcript

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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


    事前条件

    View Slide

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

    View Slide

  11. 冗長な検査はよくないよ
    渡されたデータが正しい処理のための条件を満たしてい
    るかどうか、絶えず検査することになる → 複雑さの原

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

    View Slide

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

    View Slide

  13. 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もあるらしい

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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


    Element_p element) {


    assert(self);


    assert(element);


    assert(! myStackIsFull(self));


    ...


    }

    View Slide

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


    ...


    assert(str);


    if (! urlParse(str, &desc)) return NULL;


    ...


    return self;


    }

    View Slide

  19. 不透明な型の表現
    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を
    真似した

    View Slide

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

    View Slide

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

    View Slide

  22. 利用者側から書く
    だいたい利用者側から書く
    使う側はどんなオブジェクトがあったら楽かな
    操作から必要な型を考える
    22
    ...


    StrList_p list;


    list = StrListCreate();


    for (i = 1; i < argc; i++) {


    if (! argv[i]) break;


    StrListPush(list, argv[i]);


    }


    s = StrListJoin(list, ",");

    View Slide

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


    const char * str) {


    assert(self);


    assert(str);


    /* ͋ͱͰ */


    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  27. 事前条件・事後条件を見てると
    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;


    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide