region の中のコレクションと、Trivial 階層
伸縮する Vec を置けないメモリモデルは使えない。Vec・String・Map を region に住まわせるとは、成長のたびに再割り当てするコンテナと、バンプ割り当てして一括解放するだけの領域を折り合わせることであり、そして region 版と owned 版を、一つの巧妙なパラメータ化した型ではなく、二つの別の型として保つと意図して決めることだ。
Part II は原理からメモリモデルを組み立ててきた:五つの戦略を一つの region に絞り、region が
持てるものを印づける Trivial の線、持てない値のための with、自己参照のための view 型。
だがそのすべては、そこに Vec を置けなければ無に等しい。固定形のデータしか扱えないメモリ
モデルは机上の演習だ。実際のプログラムはリストを伸ばし、文字列を組み、マップを引く。Part II
最後のこの回は、日常のコンテナを region に住まわせる話だ —— そしてそれは、もう一つの本物の
決定を迫ることになる。
成長するものを、一度きり伸びる領域に
緊張を一枚の絵で言えばこうだ。Vec は成長する:埋まると、より大きなバッファを確保し、
要素をコピーして移し、古いものを捨てる。region はその逆をする —— ポインタを前へ進めるだけで、
個別の割り当てを決して解放せず、領域まるごとがブロック終了時に一手で消える。再割り当ての
上に建ったコンテナが、決して解放しない上に建った領域の中に、どう座るのか?
答えは、二つが一見より旨く噛み合うということだ。region に住む Vec は、どの Vec もする
とおりに再割り当てする —— 新しくより大きなバッファをバンプし、要素をコピーし、新しい方を
指す —— そしてただ古いバッファを解放しない。通常のアロケータならそれはリークだ。region
ではそうでない:古いバッファはブロックが終わるまでの死んだ空間で、そのとき他のすべてと共に、
ポインタ一回のリセットで無料で回収される。region での成長は、ただ「小さいバッファを捨てて、
バンプし続ける」だけだ。個別に解放しないという region の拒否 —— 制限に見えたそれ —— が、
まさに、成長するコンテナを安価に支えられる理由なのだ。
そして成長は不可視ではない。再割り当てしうる push は、モデルの他が使うのと同じ region
割り当てエフェクトを帯びる —— それを呼ぶと region をより多く消費しうるという事実が、実行時に
発見されるのではなく、その型に書かれている。リストを伸ばすメモリコストは、見える。
Trivial 階層:何が何の中に入れ子になるか
前回の Trivial 制約が、どのコンテナが region に置けるか —— そして肝心なことに、どれがどれの
中に入れ子になれるか —— を決める規則として、ここで戻ってくる。Trivial[R] は再帰的に定義
される:
- プリミティブ(
int,bool,char, …)はどの region でもTrivial; - region 参照
&R Tは、TがTrivial[R]ならTrivial[R]; Vec[R, T]は、要素型TがTrivial[R]ならTrivial[R]。
その連鎖を辿ると Vec[R, Vec[R, int]] 自身が Trivial[R] になる —— ベクタのベクタが、丸ごと
一つの region の中で、なお一回の一括解放で回収される。この再帰こそが核心だ:「どのコレクション
がどれの中に入れ子になれるか」は別の規則集ではなく、ただ要素型に適用された Trivial だ。
コンテナが region 安全であるのは、それが推移的に持つすべてが region 安全であるとき、ちょうど
そのときで、型システムがそれを無料で検査する。
その線は、何が中に入れないかも明確に言う。owned String —— heap 裏づけで Drop を持つ ——
は Trivial ではなく、だから region に住めない。それは欠落ではない。前回の Trivial/Drop
分割が仕事をしているのだ。文字列にはモデルが代わりに二つの region ネイティブな形を差し出す:
不変の &R str、region に住むスライスで、その部分文字列も split も、みな同じ領域を指すから
自由に共有できる;そして可変の StrBuf[R]、テキストを組み上げるための Vec[R, u8] の薄い
ラッパだ。owned で heap 確保の文字列も依然として存在する —— ただ、region の中で手を伸ばす先が
それではない、というだけだ。
一つの決定:巧妙な一つの型ではなく、二つの型
これらのコンテナをそもそもどう提供するかに、本物の分岐があった。そしてそれがこの回で最も 面白い選択だ。明白な答えが、Mere が採った答えではないからだ。
統一的な選択肢 —— Rust が手を伸ばすもの —— は、アロケータでパラメータ化した一つの Vec
型だ:Vec[A, T]、ある実体化では A がグローバル heap アロケータ、別の実体化では region
アロケータ。一つの実装、一つの API、region 性は型パラメータで選ぶ。エレガントで、コード最小化
が目標ならそう書くだろう。
Mere は別の道を採った:二つの別の型。 Trivial でバンプ割り当ての region Vec[R, T] と、
heap 裏づけで Drop を持つ OwnedVec[T]、その API の読み取り部分を Collection trait の裏で
統一し、呼ぶ側はたいていどちらを持っているか気にしなくてよい。
理由は、Part II 全体を形づくってきたのと同じ本能だ。アロケータパラメータは、実行時の選択
—— どのアロケータか —— を型に通すことで、二種のベクタを同じに見せる。だが region 性は
実行時の選択であるべきではない。region の価値のすべては、その割り当てがディスパッチも
値ごとの片付けもないただのポインタバンプであることだ。owned と region をアロケータ経由で
一つの型に畳めば、Trivial な型と Drop な型をこっそり同じ名前の裏に置き、前二回が一本
かけて引いた線をぼかしてしまう。二つの型はその線を鋭く保つ:Vec[R, T] は常に Trivial、
OwnedVec[T] は常に片付けを持ち、両者が互換のふりをすることは決してない。両者間で値を移すのは
明示的なコピーであって、暗黙のアロケータ切り替えではない —— 言語がいたるところでする、隠れた
変換の拒否と同じだ。
代価はある。設計ノートははっきり述べる:標準ライブラリはいまや push を二度書く。だがそれは
一度、ライブラリの作者が払うコストで、共有 trait の裏で他の全員から隠される —— もし代案が
すべての利用者にとってのより曖昧な型システムなら、重複を費やすのにちょうど正しい場所だ。
これは Mere の選択の反復する形だ:少ない概念をこっそり併合するより、多い概念を区別したまま
保つことを好み、区別の代価を、読む人の疑いではなく作者の労力で払う。
Part II、閉じる
これでメモリモデルが完成する。それは五つの戦略の一覧として始まり、より小さく鋭い何かとして
終わった:バンプ割り当てして一括解放する一つの region;region が持てるものと、with が
LIFO 順に片付けねばならないものを分ける Trivial の線;値が自分の region の中へ安全に参照
できるようにする view 型;そしていま、解放せず捨てることで region の中で成長するコレクションと、
何がどこに入れ子になれるかを決める再帰的な Trivial 階層。どの部品も型に明示され、抜け穴なしに
検査でき、紙の上から読み取れる —— それが最初の回が掲げた狙いのすべてだった。
メモリは値がどこに住み、いつ死ぬかに答える。次の Part は、言語が同じくらい執拗に明示させ たがる別の問いに答える:あるコードは何をしてよいのか —— ネットワークに触れ、時計を読み、 ファイルを書くのか? それがエフェクトシステムで、それは副作用を無料で移動させることを拒む ところから始まる。次回:副作用を値として渡す。