view 型:region が易しくする難問
自分の別フィールドを指すフィールドを持つ値 —— 自分の text へのスライスを保持するドキュメント —— は言語設計の本物の難問の一つで、Rust は Pin と unsafe を要し、他は dependent types に手を伸ばす。Mere はそれを三つの小さな規則で解く。解けるのは、前二回で作った region がすでに難しい部分を担っていたからだ。
前二回はどちらも、同じ先送りした問いを指して終わった:region への参照を持つとは何を 意味し、それが指す先より長生きするのをどう止めるのか? 先送りされ続けたのは、その下で これが言語設計の本物の難問の一つだからだ —— Mere の答えがどれほど小さくなるかを見る前に、 なぜ難しいのかを見ておく価値がある。
自己参照する値
一度パースして、その後は自分の text へのスライスを握って保持するドキュメントを考えよう:
struct Document { text: owned String }
// 望み:text を一度パースし、トークンをそれへのスライスとして保持
view DocumentView of Document {
tokens: Slice<&self.text, str>, // self.text の中を指す
}
tokens は Document.text の中を指す。これはまさにパーサが欲しい形だ —— バッファを
トークン化し、部分文字列をコピーする代わりに安価なスライスを持ち回る —— そして地雷原
でもある。Document が動いた瞬間、text は新しいアドレスに座り、tokens の中のすべての
スライスが dangling になる。型システムは「同じ値の別フィールドへの参照」を言える必要も
あり、構築は text を先に、スライスを後に作らねばならず、破棄はスライスを text より先に
drop しないと不正な状態を通ってしまう。
これは隅のケースではなく、有名なケースだ。Rust は Pin<T> で値をその場に固定し、実際に
自己参照を作るには unsafe を要求する。dependent types を持つ言語は正確に表現できるが、
たいていのプログラムが払うべきでない代価を伴う。問題は本物で、通常の解は重い。
三つの公理
Mere の答えは意図して小さい —— 三つの規則、そして unsafe なし:
A —— view は immutable な束ねだ。 view は構築時に内部の参照関係を確定する。その後は 変更もムーブもできず、不変参照を介してのみ使われる。どのフィールドのアドレスも変わり得 ないから、そこへのスライスが古びることがない。
B —— view は region 内で構築する。 view は宙に浮いた heap ではなく region 上に住む。 その lifetime は region の lifetime そのもので、すべてのフィールドが同じ region を指すから、 フィールド間の参照はすべて、一つの運命を共にする一領域の内部で閉じる。
C —— view V[R] of T は本物の構文だ。 view は struct の派生形として、住む region で
パラメータ化して宣言する。パラメータ R は型システムが声に出して「これ全部が region R の
中だ」と言う手段だ —— 制約はあなたの頭ではなく型の中にある。
書き出せば、view とその構築子は普通のコードだ:
view DocumentView[region R] of Document {
own: &R Document, // Document 本体、region R に置かれる
tokens: &R [&R str], // own.text の中を指すスライス
}
fn make_view[region R](src: &borrowed str, r: &R Region) -> &R DocumentView[R] {
let doc = r.alloc(Document { text: copy_to_region(src, r) })
let tokens = tokenize(doc.text, r) // doc.text へのスライス、同じ region
r.alloc(DocumentView { own: doc, tokens: tokens })
}
新しい構築構文も、抜け穴もない —— ただ region への割り当てと、普通のレコードだ。
なぜ安全か、そしてそれが偶然でない理由
自己参照が通常どう壊れるかを一つずつ辿ると、そのどれもが、あなたがすでに出会った規則で 閉じられている:
- 値は動けない(公理 A)から、スライスが指すアドレスは決して変わらない。
- 変更できない(公理 A)から、構築時に組んだ関係が後で壊れることがない。
- すべてのフィールドが同じ region を指し(公理 B)、region は一括で解放する(二回前の 統一)—— だからスライスが指す先が、スライスより先に解放されることがない。破棄で通る べき不正な中間状態がない。領域まるごとが一手で消える。
- view は region の外へ持ち出せない(型の中の
R、公理 C)から、それが指す先より長生き する経路がない。
際立つのは、このどれほど少しがview についてかということだ。安全性のほとんどは、region が
すでに持っていた性質から来る:動かない、一括で解放する、そこへの参照は R で型づけられる。
view の規則が足すのは「immutable、そして of T で宣言」だけ。他の言語に Pin と unsafe、
あるいは dependent type システムを要する問題が、Mere には三つの小さな規則で済む —— 前の
回で作ったメモリモデルが、すでに重さを担っていたからだ。これが、二つの概念を一つに畳んだ
ことが下流で報われる姿だ:次の難しい機能が、ほとんど無料になる。
ここには言語が何度も立ち返る思想的な点がある。Rust の自己参照への正直な答えは unsafe
だ —— 保証に開けた明示的な穴で、正直ではあるが、穴は穴だ。Mere はむしろ、穴を要さなく
なるまで機能を制約する:view は immutable・region 束縛・ムーブ不可でしかあり得ず、その
代わり抜け穴なしで検査できる。「何でも、どこでも、unsafe で」より表現力は低く、それが
意図してなされる取引だ —— それが実際にカバーするケース(パース木、文字列インターン、
ドキュメントとそのインデックス)を、型から読み取れる保証つきでカバーする。
view の実行時コスト
ほとんど何もない、そして満足のいく理由で。view のすべてのフィールドが region 内参照か
プリミティブなら、view 全体がその region で Trivial になる —— 前回の制約だ。だから view
自身は片付けを要さない:region が回収されるとき、そこの他のすべてを解放するのと同じ
ポインタ一回のリセットで回収される。型レベルの話と実行時の話が揃う —— view が安全である
ことと無料であることは、同じ理由、すなわち一つの region と生き死にを共にすることから来る。
Part II は Mere に明示的で検査できるメモリモデルを与えることを目指し、view 型でその形は
完成した:五つの戦略を統一 region へ絞り込み、region が持てるものと with が片付けるべき
ものを分ける Trivial の線、そしていま、region の中に留まることで安全にした自己参照。
残るのはそれを使えるようにすること —— プログラムが実際に手を伸ばす日常のコンテナだ。次回:
region 対応のコレクション、そしてどれがどれの中に入れ子にできるかを決める Trivial
階層。