C++20 Contract

そういえば最近はC++関連のことに触れていないなと思ったので、今日はまだ日本語の説明があんまりないように見えるContractでも取り上げてみたい。サーベイ不足だったら申し訳ない。提案に関する情報は、

P0542R3: Support for contract based programming in C++

by G. Dos Reis, J. D. Garcia, J. Lakos, A. Meredith, N. Myers, B. Stroustrup

に基づく。ただし、この提案はまだ固まっていないため、今後も変更される恐れはある。

とか言ってたら最新のドラフトにもう入ってる上にちょっと変わってるじゃねーか!!

最後に変更点とか書いたので許して

ちなみに私はContract programmingどっぷりなどということは一切なくて、昔ちょっと触って「ほーん便利だなー、でもBoostとはいえマクロでこんなゴリゴリやるのはつらいなー……やめとこ」となってしまった人間だ。なのでContract Programmingを崇めている人からしたらテキトー言ってるしれないことをdiscraimerしておく。

ちなみに、Boost.Contractに関しては日本語の情報もそれなりに出て来る。Contract Programmingそのものに関してもかなり情報があるので、「Contract programmingとは」の部分は読まずに普通にそっちを見てもらっても構わない。

Contract programmingとは

とはいえ最初から規格の説明をするのではわけがわからなくなるので、先に Contract Programming とはなんぞやという話を(私の主観込みで)やっておこう。だいたい知ってる、という人はスルーしていただきたい。

Contract Programming(契約プログラミング), あるいは Design by Contract (契約による設計)は、雑に言うと関数などを呼ぶ時に満たされているべき制約(事前条件)、返るときに満たされている制約(事後条件)、何回呼んでも変わらない条件(不変条件)などを明示的に書いていくスタイルだ。Contract Programming のための機能を言語側で提供することにより、簡単にチェックを入れることができたり、コンパイルオプションでチェックのレベルを変えたりできる。コンパイラが賢ければ、そのような関数が連なっている時に必ず満たされる制約はチェックを自動で外したり、果ては守りようのない制約をコンパイルエラーで落としたりできるだろう。

事前条件は、関数を呼び出す際に守られているべき条件のことだ。通常は引数について期待される条件になる。簡単な例は、

  • 数値計算をする関数が、数値が inf や nan でないことを期待する
  • 文字列処理をする関数が、UTF-8エンコーディングされていることを期待する
  • 配列の要素を検索する関数が、配列がソートされていることを期待する
  • コンテナの先頭を返す関数が、コンテナが空でないことを期待する

などだ。Contract programmingをサポートする言語では、これに専用の記法が導入され、コンパイルオプションや実行時フラグなどによってアサートがされたりされなかったりする。

事後条件は、関数が返る時に満たされるべき条件のことだ。通常は返り値について期待される条件になる。簡単な例は、

  • 数値計算をする関数が、結果が必ずある範囲に収まることを保証する
  • 要素を検索する関数が、見つかったなら返却値は元の配列に含まれていることを保証する
  • コンテナの要素をフィルタする関数が、戻り値は全て条件を満たしていることを保証する

これも専用の記法によってアサートのレベルを色々といじることができるようになる。関数を実装する時は、条件が複雑すぎてめんどいということはあれど、戻り値が満たしている条件が全くないということはあまりないと思う(そういう関数は、一体何をするのか不明確すぎる気がする)。

不変条件は、プログラムを実行しても変化しない条件のことだ。その関数が変化させないもの、その実行を通して真であるような条件を指す。これをチェックする場所は難しいと思う。任意の瞬間に成り立っているはずの条件がある場合、完璧にチェックするためには1命令ごとにチェックするべきだが、そんなことをするとプログラムが圧倒的に遅くなるのでそういうことは普通しない。大抵は、怪しいところにassertを入れるという形になる。

どれも、ドキュメントやコメントのような強制力のない媒体に書くよりもコードに入っていた方が圧倒的に良いことは疑いようもない。どの条件が満たされなかったかで、関数の内部にバグがあるか、関数を呼び出した側が下手を打ったが明らかになる(正しい条件を書けていれば)。通常のテストよりも少しだけ情報量が多くなり、問題のある箇所が絞りやすくなる。

さらに、これらの条件に特別な記法が導入されてコンパイラが理解できるようになったなら、十分賢いコンパイラはプログラムの妥当性をより適切に評価でき、またチェックをしつつも回数を最低限に留めることでパフォーマンスと両立することも可能になって来ると期待される。例えば、最初の関数の事後条件で「返り値が必ず偶数になる」と保証されていたとして、その返り値をconstで受けとった後「引数が偶数であることを期待する」事前条件を持つ別の関数に渡すとする。通常のassertでContract Programmingを真似ていたなら、2回チェックが挟まるだろう。最初の関数の最後と、2番目の関数の最初だ。だが専用構文が用意されていれば、十分賢いコンパイラは2番目の関数の最初にチェックは要らないことに気づくかもしれない。もしそうなれば、不要な2度目のチェックだけが省略されて、頑健で高速なバイナリが完成する。あるいは、その変数を「引数が偶数でないことを期待する」関数に渡した場合、コンパイラは絶対にそれが動かないことを指摘してくれるかもしれない。これは結構嬉しくないだろうか。

実際にコンパイラがそのレベルまで賢くなるかはわからないが。あるいは、十分賢いコンパイラは普通のassertでも明らかに通るとわかる時は取り除くかもしれないが。

C++20 Contractの記法

というわけで提案文書を見ていこう。

概要

条件を書くのには、attributeの構文が用いられる。

int f(int x)
[[contract-attribute (contract-level): conditional-expression]]
{
    // function body ...
}

Contractはfunction typeに対して適用されるため、ここで示されている位置に来なければならない。

ここで、contract-attributeは以下のどれかだ。

  • expects: precondition(事前条件)
  • ensures: postcondition(事後条件)
    • これのみ、contract-levelの後にidentifierを書くことができる。その名前で関数の返り値を参照できる。任意の名前をつけてよい。
  • assert: assertion(関数内に書かれる普通のassertと似たような感じ)

contract-levelは省略できる。書く場合は以下のどれかだ。

  • always
    • どんなビルドモードでも検査される。その性質上、検査のコストは無視できることが期待される。assertに対してしか使用できない。
  • default
    • 何も書かなかったらこれになる。default、またはauditモードで検査される。関数そのものの実行コストと比べると軽い(が本気の最適化の邪魔にはなる程度の)チェック。
  • audit
    • auditモードでのみ検査される。関数そのものの実行コストと比べても見劣りしないかより重いチェック。デバッグの時にチェックしたいような条件。
  • axiom
    • 検査するまでもないレベルの条件で、ほとんど「正式なコメント」扱い。いかなるモードでもチェックされない。これ要る? メモ用かな?

alwaysに関しては少し揉めたっぽくて、ミーティングで投票などが行われている。

conditional-expressionは、満たされるべき条件式だ。そこにどんな式が許されるかの詳細に入る前に、簡単な例をいくつか提案文書から引用しておこう。

int f(int x) noexcept
  [[expects audit: x>0]]
  [[ensures axiom res: res>1]];

関数fは、引数xが0より大きいことを期待しており(expects audit: x>0)、返り値resが1より大きいことを保証している(ensures axiom res: res>1)。ただし、事前条件の検査はauditモードでないとなされないし、事後条件はaxiomなのでいかなるモードでも検査されない。

void g() {
  int x = f(5);
  int y = f(12);
  //...
  [[assert: x+y>0]]
  //...
}

関数gは、x + yが正であることを表明している。レベルはデフォルトなので、defaultモードとauditモードで検査される。

ちなみに、これらの単語(auditaxiomなど)を別の文脈で使うことは許される。[[expects audit: autit == 0]]のようにも使える。この構文の中のどこに出現するかによって、パーサーはこのトークンが何を意味しているかわかるはずだからだ。

とまあ、こんな感じだ。ではもう少し深く潜ってみよう。

契約違反時の挙動

条件が破られると、violation handlerが呼ばれる。ただし、以下の関数の名前は定められておらず、ユーザーが直接呼んだり、関数ポインタを渡してそれに差し替えるというようなことはできない(のだが、なぜかユーザーが指定した場合の話もされている。意見が分かれているのだろうか)。差し替えることが簡単にできるならセキュリティ的な問題が発生しかねない、と筆者らは主張している。

void _Violation_handler(const std::contract_violation &);

ここで、このstd::contract_violation<contract>ヘッダで定義されている構造体で、以下のようなものだ。

namespace std {
  class contract_violation {
  public:
    int line_number() const noexcept;
    string_view file_name() const noexcept;
    string_view function_name() const noexcept;
    string_view comment() const noexcept;
    string_view assertion_level() const noexcept;
  };
}

概ね自明な関数名だが、commentだけ不明瞭かもしれない。ここには破られた条件を文字列にしたものが入っている。ソースコード内のコメントではない。

条件が破られた時、このクラスは、assertが失敗した時はその[[assert]]が書かれていた場所を、関数の事前条件([[expects]])が破られた時は条件を破って関数呼び出しを行った場所を、関数の事後条件([[ensures]])が破られた時はその関数の定義部分を、それぞれ指すようになる。

さらに、違反時の挙動はもう一段階制御可能で、violation continuation modeと言うのをビルド時にon/offできる。デフォルトはoffで、その場合ハンドラが呼ばれたのちstd::terminateによってプログラムは終了する。もしonにしたなら、ハンドラが呼ばれたのち、プログラムは継続して実行される。これは、主にログを取る目的で導入されたようだ。

さて、実際に使用するためにはもう少し情報が要るはずだ。もう一段深く潜ることにしよう。

関数のredeclarationと継承

関数の前方宣言をするとか、inline関数が入ったヘッダを複数回読み込むとか、関数の宣言が複数回現れることは少なくない。このような場合、その関数の事前・事後条件は、「1. 完全に同じ条件である」か、「2. 完全に省略される」必要がある。

int f(int x) 
  [[expects: x>0]]
  [[ensures r: r>0]]; // 前方宣言

int f(int x); // OK. 全て省略されている
int f(int x)
  [[expects: x>0]]; // Error. 条件が足りない
int f(int x)
  [[expects: x>0]]
  [[ensures r: r>0]]; //OK. 同じ条件

こうなると困るのが、「同じ条件」とはどういうことかということだ。まず、条件のレベルが一致している必要がある。そして、順番も等しい必要がある。ここまではいい。では条件式の同一性はどう定義されるのか。文字列レベルで同一である必要があるのか、構文木レベルで同一であれば良いのか?

この提案では、文字列レベルで同一であることを要求すると引数の変数名などに気を配る必要があって窮屈だが、論理的に同じ構造をしているだけで良いことにするとコンパイラ製作者が死ぬので、その中間、変数の名前を変えることのみ許容する、という落とし所を見つけている。

int f(int x) 
  [[expects: x>0]]
  [[ensures r: r>0]];

int f(int y)
  [[expects: y>0]]    // OK.
  [[ensures z: z>0]]; // 名前は違うが、構わない

だいたいわかってきた。だがもうちょっと知っておかないと不安になる。もう少し行こう。

条件式として書ける式

条件式には、ほとんどどんな式でも書ける。

int f(std::vector<int>& v)
  [[expects: v.size() >= 10]]
  [[ensures: !v.empty()]]; // OK.

ただし、変数を変更した場合、その動作は未定義になる。

int f(int x) 
  [[expects: --x>0]]; // undefined! xを変更した

constexpr関数に対する条件式で、コンパイル時定数でないものを使ってはならない。どのタイミングで実行されるかを考えると当然だろう。

int min=-42;

constexpr int g(int x)
  [[expects: min<=x]] // Error. minはコンパイル時定数ではない
{/*...*/}

条件が複数並んでいたら、上から実行される。

void f(int * p)
  [[expects: p!=nullptr]]
  [[expects: *p == 0]] // p!=nullptr の後に実行されるのでセーフ(?)
{
  *p = 1;
}

これ、violation continuation modeonだとめでたくUBな気がするが……? それとも、p!=nullptr && *p == 0のような扱いになっていて前半で落ちた場合後半ではviolation_handler呼び出しは発生しないのだろうか?

std::tupleなどを返す関数の返り値についての条件では、構造化束縛を使える。

std::tuple f() 
  [[ensures [x,y]: x>0 && y.size()>0]]; // OK.

std::tuple f() // 使わなくてもよい
  [[ensures r: get<0>(r)>0 && get<1>(r).size()>0]];

[[ensures]]で使っている値を、関数の中で変更してはいけない。function bodyとあるのでどうやら一切変更してはならないようだ。なぜだろう。処理系はreturnするべき値をレジスタに載せ終わった直後にチェックを挟むのだと思っていたが違うのだろうか。

int f(int x)
  [[ensures r: r==x]] 
{
  return ++x; // Ill-formed: xが変更された
}

クラス内の関数の契約条件は、呼ぶ側がアクセスできない変数や関数を使ってはいけない。「その関数が」アクセスできない変数や関数ではないので注意。これは少しわかりにくい。

class X {
public:
  int v() const;
  void f() [[expects: x>0]];   // Ill-formed.
                               // f()は外部から呼ばれ得る
  void g() [[expects: v()>0]]; // OK. v()はpublic
protected:
  int w();
  void h() [[expects: x>0]];   // Ill-formed.
                               // h()は継承先から呼ばれ得る
  void i() [[ensures: y>0]];   // OK. yはprotected
  void j() [[ensures: w()>0]]; // OK. w()はprotected
  int y;
private:
  void k() [[expects: x>0]]; // OK.
        // k()はprivateなので内部からしか呼ばれ得ない
  int x;
};

class Y : public X {
public:
  void a() [[expects: v()>0]]; // OK
  void b() [[ensures: w()>0]]; // Ill-formed
  void c() [[expects: x>0]]; // Ill formed
protected:
  void d() [[expects: w()>0]]; // OK
  void e() [[ensures: x>0]]; // Ill-formed
};

また、継承先で関数をオーバーライドする場合、継承元の事前・事後条件も引き継がなければならない。

関数ポインタには事前・事後条件を与えることはできない。ただし、事前・事後条件をもつ関数のアドレスを関数ポインタに代入することは可能で、その場合もチェックが行われる。

typedef int (*fpt)() [[ensures r: r!=0]]; // Ill-formed

int g(int x) 
  [[expects: x>=0]] 
  [[ensures r: r>x]]
{
  return x+1;
}

int (*pf)(int) = g; // OK.

まとめ

と言うわけで、C++20に入ることになったContractの提案文書の内容をまとめた。

だいたい書き終わって細かい表現とか表記揺れとか統一しようかな、というタイミングで、最新ドラフトN4800にすでにContractが入っており(§9.11.4)、その上少し変更されていることに気がついた。ざっと見た限り、alwaysは結局なくなっている。あと、violation handlerの設定方法は処理系定義に落ち着いたようだ。

あとは基本的には変わっていない。とはいえ規格なので、超細かい話が追加されている。例外送出やlongjmpでの関数からの脱出では事後条件はチェックされないことや、Contractの条件式が常に同じ値を返す場合は同一性のルールに違反していても警告する必要がない(x < yy < xに入れ替わっているというような場合)、などが追加されている。

ちゃんと最新のドラフトを追っていないと、アホな目に逢うということを痛感した回だった。

ともあれ、すでにドラフトに入っている程度には前向きに検討されているようなので、強化版assertとして今後使っていくことを考えてみる時期ではないだろうか。

AIアシスタントの中身はどうなっているんだろうか

たまには下調べや引用などをせずに思ったことを適当に書き散らしてみたいと思う(いつもでは?)。なので、この記事には裏が取られた情報や5秒ググればわかることなどは書かれていない。すべて想像で書いている。

昨日Siriに少し話しかけており、ふとこいつはどうやって実装されているのだろうと思った。まず、音声認識から文字列に起こす。これは古典的なものから機械学習まで先行研究がたくさんあるだろう。得られた文字列を形態素解析して構文木的なものを作るのもよく研究されている。だから、この方法を取るなら既存研究をうまく活かして、よく分離されたコードで実装できるだろう。

その後はどうしているのだろう? 「近くの喫茶店を探して」のように命令形のものは形態素解析の結果から動詞(「探す」)と目的語(「喫茶店」)やそれを修飾している語(「近くの」)を取り出して、「探す」だからWebで検索、Googleに「喫茶店 近く GPS座標」を入力、とする……。しかし、これを実装する方法はたくさんあるだろう。自然言語の動詞はあいまいだ。「探す」は目的語によってするべきことが変わってしまう。喫茶店を探すならWebで検索することになるが、IoT家電を導入したので接続できるものを探して、という場合は? あるいは、PCの中にあるはずの家族写真を探して、だったら? テーブル引きにせよswitchmatchにせよ、早晩無理が来るように思える。

まあ、まだPCやiPhoneに関係する仕事だけなら、動詞からありえる操作が入った関数ポインタへのテーブルを作っておくのは不可能ではないだろう。自然言語による表現は揺れるものなので、単に動詞だけを与えられて必要な操作を唯一つ選び出すのは不可能だろうが、操作を意味する関数側が受け取れる目的語を選別できるように実装されていれば、一度テーブル引きをしたあとは総当りでも不可能ではない量になりそうだ。それに、ユーザー個人の履歴を残しておけば、この検索もより高速にできる。データベースを動的に変えていいなら、ユーザーに多少の設定をさせれば、好きな語彙から操作を呼び出すこともできそうだ。

だが、AIアシスタントはIoTの方に向かいつつあるように見える。確かに、人間ならば朝起きて電気のスイッチまで歩くよりも、ベッドの中から「光あれ」と呟いて電気を付けたいものだろう。家を出てから「鍵閉めたかな」と不安にもなるだろうし、実際に鍵が開いていたら遠隔で閉じたい。実際、そういったテクノロジーの断片がかろうじて射程に入り始めた瞬間には既に、こういったアイデアSF小説や漫画、娯楽雑誌などによってもてはやされていた。

さて、将来的にIoTに対応しようと考えるなら、上記のような実装は少し……というか大分、つらい気がする。素朴に考えるなら、「Hey Siri、暖房付けて」と言ったらSiriがエアコンに対して、リモコンで暖房をつけるときに送られる赤外線信号と同じようにエンコードされたビット列を同じく赤外線で送ればよい、と思ってしまう。確かにこれは動くが、実現可能かと言われれば否だろう。これを実装するには、Siriがありとあらゆるエアコンの赤外線信号と動作の対応表を持ちつつそれを内部のテーブルに保持しておく必要があるからだ。これは単純に不可能である。あるいは、ユーザーがSiriが入っているデバイスを購入したあと、所持しているエアコンの赤外線信号をリバースエンジニアリングし、エンコード方法をSiriに登録する必要がある。これは知識と意欲のあるニンジャ級ハッカーならできるだろう。が、多くの消費者はニンジャ級ハッカーではない。

となると、取り得る戦略はかなり少なくなる。少なくとも、家電側が歩み寄りを見せ、AIアシスタントの中央集権をやめないといけないだろう。家電がIoT対応をして、何らかの方法でそれを操作するインタフェースを提供し、AIアシスタントがそれに則って仲介役を果たすという形なら現実的になってくる。残る問題は、どこでAIアシスタントが処理を止めるか、言い換えると、各家電がどこまでやるかだ。

極論、各家電がそれぞれスマートアシスタントを持っていれば、Siriの仕事はなくなる。「Hey エアコン、暖房22度ね」で済むなら、「Hey Siri, 暖房22度にして」を聞いたSiriは単に同じ文章を、あるいはもう音声信号そのままを周囲の家電全員にブロードキャストすればよい。よくわからなかった家電はSiriにエラーを返し、Siriは全員が失敗したときだけ失敗を伝えればよい。Siriの実装コストは極端に下がる。が、各家電の実装コストは跳ね上がるし、Siriの存在意義が怪しい。

逆の極端として、各家電はシリアライズされたデータを受け取り、それを内部で展開すると考えてみよう。これはリモコンが赤外線信号を送っているのと同じだ。だがそのままだと先述の通りうまく行かないので、ニンジャ級ハッカーがするであろうことを肩代わりする必要が出てくる。つまり、各家電が自身に送ってほしいデータのシリアライズ方法を説明できるようにしておき(例えば、JSONで送ることにしておき、必要なフィールドと型を指定するなど)、それをSiriに先に伝えておくのだ。あるいは、そのような処理をパッケージ化したデバイスドライバを同梱しておき、購入後AIアシスタント側にそれがインストールされるようにする。そうすれば、Siriは自然言語処理をしたあと、iPhone側で行うことと同様の流れで処理ができるようになる。こっちの方が現実的だろう。

この場合に問題になることがあるとしたら、まず全家電と全AIアシスタントが同意したプロトコルを先に決めておく必要があることだ。家電メーカーもAIアシスタントメーカーも、自社のプロトコルをそのままデファクトにしてしまいたいという欲求をちゃんと抑えて、それぞれのプロトコルの長所短所をしっかりと認識して突き合わせ、止揚しなければならない。それを国際規格にして、以降全員が(ある程度拡張はされるだろうが)その国際規格に準拠する。規格制定には時間がかかる気がするが、ちゃんとできればよい方法だ。既存の何かに乗っかってもよいわけだし。あと、この方法だとと、家が巨大になるとスケールしない可能性があるのではないか。というのも、家電が増えるに連れてAIアシスタントが持っている可能な操作のテーブル、あるいはデバイスドライバのリストがどんどん肥大化するからだ。高性能でたくさんの可能な操作を持っている家電を大量に買う富豪の家に買われたAIアシスタントは大変だ。受け付けるべき命令が増え続け、それでも人間を待たせてはいけない。なんだかんだ現実的な範囲では問題にならないかもしれないが、少し不安ではある。これから先、どんな奇妙な家電が増えるかわかったものではないからだ。

先の2つの解決策の中間に、「構文解析を終えた命令文を直接対象の家電に送る」というものがある。Siriは音声認識構文解析をし、自身がするべき操作(Webで検索、5分計るなど)は今まで通りこなし、自分ができる操作ではないと判断したら、できそうな家電に命令の構文木をそのまま送る。各家電は、構文木を渡されると自分で何をすべきか解釈する。すると、AIアシスタントのするべきことはかなり減る。家電側の実装コストは高くなるが、前処理済みの構文木が渡されるならスマートアシスタントを実装するのに比べるとするべきことは十分少ないだろう。また、このやり方なら家電の数が増えてもSiriがするべき仕事があまり増えないという利点もある。最悪全員にブロードキャストすればよいのだし、関係のありそうな家電にのみ送るにしても、キーワードから家電のIPへのテーブルさえ持っていればよい。家電の種類に線形にテーブルサイズは大きくなるが、家電の機能に比例してテーブルが大きくなる心配はない。いいとこ取りのような気もするが、面倒そうなところも両取りになっており、最終的には手放しで喜べる状況でもなさそうだ。この場合も規格制定は必要で、特に構文木を送るプロトコルを定める必要があり、結構面倒な気がする。家電メーカーも中途半端に賢いドライバを書かないといけない。三方一両損な形だ。やはり、各家電がAIアシスタント用のドライバを持っていて、購入時にインストールされるようにしておくのがよいのだろうか。

しかし、ずっとハッシュテーブルのようなものを持っておくと想定して考えてきたが、ここの速度と精度は性能に聞いてくるので、いろいろ試してみるべきだろう。「構文木から必要な操作を取り出す」というタスクを、「パターンに対応するラベルを貼る」という操作に置き換えることを考えると、流行りの機械学習が強みを発揮しそうな気がする。さらに、ユーザーがある程度アシスタントの動きを訂正してくれるなら、オンザフライでユーザーの癖を学習できるかもしれない。とはいえ、ユーザーが懇切丁寧に子供を育てるようにAIアシスタントに言葉とするべき仕事を教えてくれるとは思えないので、ある程度汎用で機能するような重みを先に学習して持っておく必要もある。どうせテーブル引きの場合でもテーブルを作る必要があるので、教師データを作るのが面倒ということもないだろう。ただ、テーブルがそんなに大きくならず、検索もそう難しくないとかならわざわざ学習をするまでもない。実際どういう実装になっているんだろうか。

というようなことを考えて、「Hey Siri, ソースコード見せて」と聞いてみたが、はぐらかされてしまった。今は色々と話しかけつつ、うまく口説き落とせないか試してみている。

Rust Book 勉強会 #3 フォローアップ

Rust Book 勉強会で発表をしてきた。

rust-kansai.connpass.com

これは、結構前にQiitaのC++アドベントカレンダーで書いた内容と、Ben Deane氏によるCppConでの発表"Using types effectively"が下敷きになっている。

www.youtube.com

あ、この資料にクレジット書き忘れたのを思い出した、やっちまった

さて、その中で以下のRedditで出ていた話も紹介した。

www.reddit.com

スレ主の質問は、以下のenumのサイズが20になるのは何故かというものだ。

enum Vector {
    V2(f32,f32),
    V3(f32,f32,f32),
    V4(f32,f32,f32,f32),
}

答えは、データのレイアウトが以下のようになっているからだ。

|tag|padding|data region|
|u8 |[u8;3] |[u8;16]    |

データ領域は最大のものが格納できるサイズになり、どの値が入っているかを示すタグがきて、データ領域のアライメント調整用のパディングが入るというものだ。

さて、これは本当にこれ以上縮まないだろうか。

16バイトで表現できないのか?

できる気がしたので記事を書くことにする。ところでRustのf32とかf64ってIEEE754に従うよね?

NaNはご存知だろうか。Not A Numberの略で、値がもはや数値ではない場合に使う。例えば、0.0 / 0.0の結果などがNaNになる。このNaNは特殊な値で、それとの大抵の演算結果がNaNになり、またNaNに関するあらゆる比較は偽になる。NaN == NaNすら偽になる。

このNaNはexponentのビットが全て1で、mantissaが非ゼロという定義になっている。つまり、mantissaのほとんどは手付かずのまま残るということだ。mantissaのビットパターンを変えても、NaNのままにしておける。

そして、mantissaが割と自由に決められるということは、そこに情報を押し込めるということだ。これはNaN Boxingと言われており、言語処理系でたまに使われる。

つまりだ。Rustには、NaNの値として特別な値を準備しておき、先のenumV2に値が入っていたときに残りのf32tagged-NaNにしておいてV2であることを表現するという手段が取れるだろう。ではV4(1.0, 1.0, NaN, NaN)とどう区別するかというと、通常の演算で作られるNaNとは異なるビットパターンを持つNaNtagged-NaNとして使っておけば、区別できるのではないか。

|f32|f32|tagged-NaN|tagged-NaN|
|f32|f32|f32       |tagged-NaN|
|f32|f32|f32       |f32       |

まあ、これを実現するためにはアーキテクチャが通常の演算から返すNaNのビットパターンが固定されている必要と、NaNの演算結果でNaNのmantissaのビットパターンが変化しないことが保証されている必要がある。

どうなのだろう。そこのところをよく知らない。

とにかく、Rustは未だに上記のenumのサイズを20バイトにする。ここで書いたことが実はハードウェア的に実装不能なのか、それとも実装がややこしすぎる、あるいは手間に対して利益が少なすぎるとみなされているのか。私が気づくようなことがまだ提案されていないとはあまり思えないが、はて。

Rustで型推論を助けるトリック

Rust業界に詳しくないので既に広く知られているものかもしれないが、昨晩突如として以下のようなトリックを思いついた。

struct X<T> {/* fields omitted */}
impl X<f32> {
    pub fn f32(self) -> Self {self}
}
impl X<f64> {
    pub fn f64(self) -> Self {self}
}

let x = X::new(/* ... */).f64()
//                       ^^^^^^
// 他の関数呼び出しなどからは無理でも、これのおかげでT = f64と推論できる

いや、明示的に型指定しろよ、それかデフォルト型パラメータを使えよ、と思う方が大半であろうが、以下のようなケースで便利になるのではないかと思う。というか困っていたから思いついた。

まず、何らかの物体の三次元座標が入ったファイルがあり、それを読み込みたいと思っているとする。

A     1.00 1.00 1.00
B     2.00 3.00 1.00
...

これを読み込む際、例えばデータ量が超絶多いとか、可視化できればそれでいいから精度は低くていいとか、あるいはその両方といった理由でf64ではなくf32を使いたいケースというのはあるだろう。そういう場合、まあcfgでビルド時に決めてしまってもいいのだが、ジェネリクスを使って解決することもできる。

struct Particle<T> {
    name: std::string::String,
    pos : [T; 3],
}

で、これを読み込みたいとき、例えばちょっとLazyにしたかったりするだろう。そういうときはIteratorimplするのがよさそうだ。

struct Reader<R> { /* fields omitted */ }

impl<R: std::io::Read> std::iter::Iterator for Reader<R> {
    type Item = Particle<T>;
    //                  ^^^
    // この型パラメータTはどうするのか?
    fn next() -> Option<Particle<T>> {
        // ...
    }
}

すると、IteratorItemに型パラメータが必要になる。だが、Readerは今Particleの型パラメータTを知らない。なのでTを決めようがない。

仕方がないので、Readerにダミーのパラメータを持たせることになるだろう。

struct Reader<T, R> {
    _marker: std::marker::PhantomData<fn() -> T>,
    // ...
}

impl<T, R: std::io::Read> std::iter::Iterator for Reader<T, R> {
    type Item = Particle<T>;
    fn next() -> Option<Particle<T>> {
        // ...
    }
}

さて、このReadernewするとき、どうすることになるだろうか。

理想は、R: std::io::Readの方は型推論が何とかしてくれて、T: f32 | f64の方だけユーザーが明示的に指定できるというものだろう。

let reader = Reader::<f32>::new(
        std::fs::File::open("example.dat").unwrap()
    );

だがこれは通らない。C++template関数は決まっていない型パラメータだけを明示的に指定することを許すが、Rustは許さない。二つパラメータがあるなら二つ書く必要がある。

まあそれでも、「これは型推論しろ」という意思をアンダースコアで表現できるのでまだマシだが。

let reader = Reader::<f32, _>::new(
        std::fs::File::open("example.dat").unwrap()
    );

これでいいのでは? いや、考える必要があるのはユーザーのことだ。新規ユーザーがこのサンプルコードを見たら、「おや、このライブラリは柔軟性のために書きやすさと学習コストを犠牲にしているようだぞ? こんな単純なサンプルですら型推論が助けにならないなら、多分この後ずっと、どの型が何個ジェネリクスパラメータを取るか毎回調べないといけなくなるんだろうな、はーめんどくせ。なんのために型推論があると思ってるんだ。Rustやめちまえ」と思うだろう。

このユーザーは私の心の声でもある。昔書いたライブラリの構造体のジェネリクスパラメータの数なんか覚えてねえよ。コンパイラの仕事だろ。コンパイラが数えろ。

で、最初は「仕方ない、たいてい必要になるのはファイルを開いて読み込むことだから、それをするユーティリティ関数でも書いて、そこでf32f64を選べるようにするか」と考えた。

pub fn open<T, P>(path: P) -> Result<Reader<T>>
where
    P: std::convert::AsRef<std::path::Path>
{
    // ...
}

ああーっ、また型パラメータ増やしてやがる! お前はいつもそうだ。誰もお前を愛さない。

さて、そうなると、やはりこの関数を呼ぶときにも型パラメータの数を意識しないといけない。

let reader = crate::reader::open::<f32, _>(
        "example.dat"
    ).unwrap();

何も簡単になってないじゃないか!

というわけで、冒頭のトリックが有用になったりするわけだ。

impl<R> Reader<f32, R> {
    pub fn f32(self) -> Self {self}
}
impl<R> Reader<f64, R> {
    pub fn f64(self) -> Self {self}
}

これを使うとrustc型推論を補助することができる。

let reader = crate::reader::open("example.dat").unwrap().f32();

f32()を呼べるのはReader<f32, R>だけだから、T = f32に決定する。そしてopenができるのはReader<T, std::fs::File>だけだから、R = std::fs::Fileに決まる。というわけで、明示的に型パラメータを一切指定せずに、全ての型パラメータを決めることができるようになった。

別の行に書いても多分大丈夫だと思う。

if let Ok(reader) = crate::reader::open("example.dat") {
    let reader = reader.f32();
}

これちょっと格好良くないか? ちょっとビルダーパターンっぽい気もする。動的に型変えられるようになってる感があって面白い。

実際は、動的に変わっているのではなくて、rustcが型不定のまま1行めを解釈したあとreaderに対してf32()が呼ばれているのを見てそこで1行めのreaderReader<f32, File>だったことを知るのだ。なので実際には遡って型パラメータが決定されていっている。

これで型パラメータに怯えることはなくなった。型パラメータの数をいちいち数える日々から開放された。

安心して今夜も眠れるだろう。

Rustでtraitのassociated typeに対してtrait boundaryを課す

RustのTraitは内部に型を持つことがある。関連型(associated type)だ

trait Hoge {
    type Value;
}

impl Hoge for Piyo {
    type Value = Fuga;
}

このValue型についてトレイト境界を付けたい。 つまり、ある型Tがあり、型<T as Hoge>::ValueOtherTraitを満たす場合のみ使える関数を使いたい。

以下のように書けたらよいのだが。

fn foo<T: Hoge<Value: OtherTrait>>() -> ()
{
    // ...
}

残念ながらこれは通らない。

error: expected one of `!`, `(`, `+`, `,`, `::`, `<`, or `>`, found `:`
  --> src/main.rs:11:21
   |
11 | fn foo<T: Hoge<Value: OtherTrait>>() -> () {
   |                     ^ expected one of 7 possible tokens here

ではどうやって書くかというと、実は割と素直に書くと通る。

fn foo<T>(x: T) -> ()
where
    T: Hoge, // ここではまだ何も言わない
    <T as Hoge>::Value: OtherTrait // これを足す
{
    println!("hello! {}", x.value().other());
}

トレイト境界の左辺にそれ書けるんかい! と思った。

動くサンプルを置いておく。 play.rust-lang.org

さて、似たような、だが少し違うケースがある。今日実際に困っていたのはこちらだ。

まず、自分のクレートに既にcrate::error::Errorを定義してあり、よくある型についてFrom for Errorを実装しているとしよう。

pub struct Error {/* ... */};
type Result<T> = std::result::Result<T, Error>;

impl From<std::num::ParseIntError> for Error {
    fn from(error :std::num::ParseIntError) -> Error {
        // ...
    }
}

で、何らかのフィールドを持つジェネリクス構造体があるとする。それを読み込めるようにしたい。

pub struct Hoge<T> {/* ... */};

impl<T> std::str::FromStr for Hoge<T> {
    type Err = Error;
    fn from_str(line: &str) -> Result<Self> {
        // ...
    }
}

このfrom_strHoge<T>のフィールド(T型)を読むためにparseを使いたい。そのためには、まずTFromStrを実装している必要がある。

impl<T> std::str::FromStr for Hoge<T>
where
    T: std::str::FromStr
{
    type Err = Error;
    fn from_str(line: &str) -> Result<Self> {
        // ...
    }
}

それだけではない。rustcはすぐに、<T as FromStr>::Errcrate::error::Errorに変換可能でないとこの関数はコンパイルできなくなることを見抜く。なので、ここまで話してきたような、関連型に対するトレイト境界が必要になるのだ。

ところで、今回はFrom</*...*/> for Errorを実装していたのだった。とすると、これはErrorに対して実装されているので、先程までと同じような書き方はできない。 Errorがトレイト境界From<T as FromStr>::Errを満たしている必要があるからだ。 これも、そのまんま書くと通る。

impl<T> std::str::FromStr for Hoge<T>
where
    T: std::str::FromStr,
    Error: From<<T as std::str::FromStr>::Err> // これ
{
    type Err = Error;
    fn from_str(line: &str) -> Result<Self> {
        // ...
    }
}

それ左辺に書けるの!?(驚愕)

Errorは普通に定義されたただのstructだったので、それに対してトレイト境界を設けられるとは思っていなかった。トレイト境界はジェネリクスの型引数に対して課すものだと思っていたからだ。

まあでも、わかってしまえば結構そのまんまだった。試してみるものだ。


追記(1/22)

当初std::ops::Addの実装をよくわかっておらず、ジェネリクストレイトの型パラメータと関連型を取り違えていた(該当箇所は混乱を避けるため消した)。

std::ops::Addの実装は以下のようになっている。

pub trait Add<RHS = Self /* 型パラメータ */ > {
    type Output; // 関連型
    fn add(self, rhs: RHS) -> Self::Output;
}

RHSはあくまで型パラメータであり、Outputが関連型だ。RHSを書くべき場所に関連型を書けないのは当たり前だし、型パラメータにさらなる型パラメータとそのトレイト境界を書けないのも当然だ。

Rustでgenericなenumを作る

パッと出てこなかったので。

例えば、位置か速度か力かわからないベクトルがあるとする。すると、Rustならenumにするだろう。

enum CoordKind {
    Position{x:f64, y:f64, z:f64},
    Velocity{x:f64, y:f64, z:f64},
}

let pos = CoordKind::Position{x: 1.0, y: 2.0, z: 3.0}

これをf64でもf32でも使いたいとなると、ジェネリクスを使うことになる。

enum CoordKind<T> {
    Position{x:T, y:T, z:T},
    Velocity{x:T, y:T, z:T},
}

let pos = CoordKind::Position::<f32>{x: 1.0, y: 2.0, z: 3.0}

このとき、明示的に型指定する場合はCoordKind::<T>::PositionではなくCoordKind::Position::<T>になる。これでちょっと驚いた。

これに対しても普通にmatchif letが使える。その際は、普通にrustc型推論をするので普通のenumと同様に使える。

let pos = CoordKind::Position::<f32>{x: 1.0, y: 2.0, z: 3.0}
match pos {
    CoordKind::Position{x, y, z} => println!("{} {} {}", x, y, z),
    CoordKind::Velocity{x, y, z} => println!("{} {} {}", x, y, z),
}

もし必要なら、CoordKind::Position::<T>{x, y, z}のように書けば良い。

match pos {
    CoordKind::Position::<f32>{x, y, z} => println!("{} {} {}", x, y, z),
    CoordKind::Velocity::<f32>{x, y, z} => println!("{} {} {}", x, y, z),
}

間違った型を指定すると以下のように教えてくれる。

error[E0308]: mismatched types
  --> src/main.rs:15:9
   |
15 |         CoordKind::Velocity::<f64>{x, y, z} => println!("{} {} {}", x, y, z),
   |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected f32, found f64
   |
   = note: expected type `CoordKind<f32>`
              found type `CoordKind<f64>`

この型指定方法の微妙なところは、以下のようなものを定義するとよくわかる。

enum Foo<T, U> {
    Hoge{x: T},
    Piyo{x: T},
}

これを明示的に型指定する場合、以下のようにする必要がある。

let x = Foo::Hoge::<i32, f64>{x: 42};

しかも、この場合i32はともかくf64は推論しようがないので(他の行でxFoo::Piyo<i32, f64>として使うとかしていない限り)明示的に書く必要がある。

これ、やはり以下の方が直感的だったのではないだろうか。

let x = Foo::<i32, f64>::Hoge{x: 42};

まあ、文法規則は直感的かどうかよりも面倒な縛りがたくさんあるのだろうということは想像はつくが。

GitBookでドキュメントを作りCircleCIでGitHub-pagesにpushする

今更感

github-pagesに直接何か生成して置くのもCircleCIを使うのも始めてでNode.jsを真面目に使うこともなかった人間がいきなりこれをやったらどうなるかの覚書

GitBookがクラウドサービスに注力してCLIの開発は基本的にやめるよとか言ってるタイミングで自力でやるとか何考えてるんだ?

まあGitBookでなくとも(例えばmdBookでも)基本は変わらないわけだし。mdBookはとっとと多言語サポート入れてくれ。docsifyとかは開発活発なのかな?

ところで、以下のやり方だと実質1コミットしかない短命なgh-pagesブランチが作られては上書きされて消えていく。初めはMarkdownがあるからHTMLやPDFは欲しい人が適宜作れるから気にしなくていいやと思っていたが、開発が止まって生成用のツールが入手できなくなる可能性を考えると、今後もどんどんバージョンを変えていってドキュメントも更新していくであろうプロジェクトではmaster/docsに生成したHTMLを入れてそれも一緒に更新していく方が良さそうな気がするなあ。

したいこと

GitBookを使ってMarkdownからリファレンス的なウェブサイトを生成し、それをGitHub pagesに置いて表示したいが、ローカルで毎回buildするのはたるいからCIサービスを使いたい

具体的には、以下をやる

  1. Markdownでドキュメントを書く
  2. GitBookでHTMLを生成する
  3. それをGitHub pagesに置く
  4. 上記を全てCircleCIを使って自動化してmasterにpushするたびに実行する

Markdownでドキュメントを書く

頑張って書く。

GitBookを使うなら、gitbook initするとREADME.mdSUMMARY.mdができる。README.mdは最初のページになる。SUMMARY.mdは基本的にリストがあるだけで、ここには目次を書いていく。これが最終的に左側にメニューバーとして出現し、またページをめくっていくときの順番にもなる。

だいたいこんな感じ↓

# Summary

* [Introduction](README.md)
* [Usage](Usage.md)
* [Reference](Reference.md)

GitBookでHTMLを生成する

基本的にこれはお手軽だ。ただ、少しばかり用意するべきものがある。book.jsonに使うパッケージやその設定を書く必要がある。例えば、

{
    "root": "book/",
    "plugins": [
        "hints", "anchors", "fontsettings", "search", "back-to-top-button"
    ]
}

rootはドキュメントのルートディレクトリだ。先のREADMESUMMARYを入れる場所。このjsonファイルがレポジトリのトップにあるなら、book/ディレクトリ以下に全てを置くことになる。これらの他にも使ってみているプラグインはあるが、面倒なのでスルー。気が向いたら書くかも知れない。

HTMLファイルを生成する前に、これらのプラグインをインストールする。

$ gitbook install # プラグインをインストール
$ gitbook build   # HTML生成

buildするとしばらくして_book/ディレクトリにindex.htmlその他が出力される。これをgh-pagesにぶち込めばよいわけだ。

GitHub pagesに置く

GitHub pagesでは、以下の3通りの場所にindex.htmlを置くことができる。

  • masterブランチのルート
  • masterブランチのdocs/
  • gh-pagesブランチのルート

gh-pagesdocsというのは駄目だ。

さて、_book/以下にあるものを改めてルートにしてgh-pagesに置く必要がある。ウェブページに必要のないソースコードなどはディレクトリに含めたくない。どうするか。

_book/内でgit initを実行し、_bookの内容しか持っていないレポジトリを作ってしまえばよい。そうするとソースコードは含まれなくなる。そこからおもむろにgh-pagesブランチを作り、元レポジトリのgh-pagesブランチに全てを無視してpushすればよい。

# 生成されたHTMLが入っているディレクトリに移動
     $ cd _book/
# そのディレクトリをルートとしてgit init
_book$ git init
# 仮のmasterに1コミット(虚無)
_book$ git commit --allow-empty '[ci skip] init docs'
# そこから分岐してgh-pagesを作る
_book$ git checkout -b gh-pages
# そこに_book以下全てを入れる
_book$ git add .
# _book以下全てをルートに持っている状態にしてgh-pagesにコミット
_book$ git commit -am "[ci skip] update docs"
 # USER/REPOのgh-pagesに無理やりpush
_book$ git push --force git@github.com:USER/REPO.git gh-pages

力技だが、これでなんとかなる。

だが結構アレなことをするし、いちいちこれを手元でやりたくない。そもそもgit add .とかして、ローカルにある変なファイル(秘密鍵とか中学二年生の頃に書いた日記のtxtファイルとか)を含めてしまったらどうするんだ(心配性)。全世界に公開されるんだぞ。いや絶対そんなもん自分のレポジトリ下にコピーして来ないが、CIサービスでやればそもそも毎回まっさらな仮想環境が構築されるので絶対に大丈夫だという安心感がある。

というわけで後は、これをCircleCIで自動でやる。

CircleCIからgh-pagesにpush

というわけで最低限必要なものを示す。

version: 2.1
executors:
  default:
    docker:
      - image: circleci/node:8.11.4

jobs:
  deploy:
    executor:
      name: default
    steps:
      - add_ssh_keys:
          fingerprints:
            - "<fingerprint of your ssh-key for deployment>"
      - checkout
      - run:
          name: install dependencies
          command: |
              npm install gitbook-cli
              git config --global user.name  "Your Name"
              git config --global user.email "Your Email Address"
              ./node_modules/gitbook-cli/bin/gitbook.js install
              ./node_modules/gitbook-cli/bin/gitbook.js build
              cd _book/
              git init
              git commit --allow-empty -m '[ci skip] update docs'
              git checkout -b gh-pages
              git add .
              git commit -am '[ci skip] update docs'
              git push --force git@github.com:USER/REPO.git gh-pages

workflows:
  setup_and_deploy:
    jobs:
      - deploy:
          name: update docs
          filters:
            branches:
              only: master

どこで何をしているかある程度言わないと不親切だが、私は何もわからない。俺達は雰囲気でCIサービスを使っている。

まあ、なんかCircleCI(2.1)はexecutorとして何かの言語のdockerイメージを使うらしく、これを先に設定しておくことで後々似たような設定を何度も書かなくて済むとのことらしい。npmを使いたいのでnodeを指定する。

続いて、jobsというのとworkflowsというのがあり、workflowsではjobsに登録されているものの依存関係やどういう順序で流すかなどを指定できるらしい。つまり、テストして、それが完了していたらそこで作ったバイナリをサーバーに置くとか、そういうことができる。まあ今回は使わないんですけど。

なので今回はworkflowにはdeployしか置いていない。で、deployというjobjobsの中に入れる。まず、そこで使うexecutorを指定。これはexecutorsで指定した名前(今回はdefault)で指定する。で、stepsのところに実際何をしていくかを書いていくようだ。

まず、add_ssh_keysfingerprintsを設定する必要がある。というのも、CircleCIのデフォルトのdeploy keyはRead onlyなので、gh-pagesgit push --forceなどとてもできないのだ。なのでこちらで新しくそのためだけのssh鍵を作り、登録してやる必要がある。

GitHubのレポジトリの設定から「Deploy keys」みたいなとこに飛び、「Add key」で鍵の公開鍵を書く。ここで小さなチェックボックスがあり、チェックすると書き込み権限を与えられるようになっている。厳重だ。忘れかねないので注意だ。

今度はCircleCIの「jobs」から目的のレポジトリのjobに飛び、歯車アイコンをクリックして同様にSSH鍵を登録する。CircleCIがこの鍵を使ってGitHubにpushするので、秘密鍵をコピペする必要がある。なので面倒だからと自分が普段使っている鍵を流用しようとしないように。

で、何故かこれだけだとCircleCIは足してあげたSSH鍵を使おうとしない。なので、明示的にadd_ssh_keysで足した鍵のfingerprintを指定する必要がある。忘れたら or 確認したかったらGitHubなりCircleCIで登録されてる鍵のfingerprintを見ればよい。

それをしておいたら、checkoutによってレポジトリのコードをcheckoutしてきて、それから実行するコマンドを羅列する。command:のところに書くと良い。基本的に上の感じで大丈夫だが、npmのためにpackage.jsonを書いたりgh-pagesなるパッケージをnpmで入れたりするとこの辺の操作が短くなるらしい。初心者なのでそれらが何をしているか知らずなんとなく気持ち悪いので全部手で書いた。

上手くいったらGitHub pagesにGitBookで生成した内容が表示される。確認した後、レポジトリのURL欄にでも貼ると良い。

ちなみにこれを実行するとgh-pagesブランチが突然現れた虚無のmasterからにゅっと生えたブランチということになり、GitHubのNetworkで見ると不連続になっている。ちょっとおもしろい。

ちなみに中学の頃の私は日記を書けるほどマメではなかったので中2の頃の日記のtxtファイルというものはない。