view 型:region が易しくする難問

自分の別フィールドを指すフィールドを持つ値 —— 自分の text へのスライスを保持するドキュメント —— は言語設計の本物の難問の一つで、Rust は Pin と unsafe を要し、他は dependent types に手を伸ばす。Mere はそれを三つの小さな規則で解く。解けるのは、前二回で作った region がすでに難しい部分を担っていたからだ。

merememory-modelview-typesregionslanguage-design

前二回はどちらも、同じ先送りした問いを指して終わった:region への参照を持つとは何を 意味し、それが指す先より長生きするのをどう止めるのか? 先送りされ続けたのは、その下で これが言語設計の本物の難問の一つだからだ —— Mere の答えがどれほど小さくなるかを見る前に、 なぜ難しいのかを見ておく価値がある。

自己参照する値

一度パースして、その後は自分の text へのスライスを握って保持するドキュメントを考えよう:

struct Document { text: owned String }

// 望み:text を一度パースし、トークンをそれへのスライスとして保持
view DocumentView of Document {
  tokens: Slice<&self.text, str>,   // self.text の中を指す
}

tokensDocument.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 で宣言」だけ。他の言語に Pinunsafe、 あるいは 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 階層。

← Back to Mere: 言語を作る