rryの期間が想像より長い理由
まずこれはNLLが関係しています。実際にNLLを有効にするとコンパイルが通ります。
rust
1#![feature(nll)]
2
3fn foo<T>(_x: T, y: T) -> T { y }
4
5fn main() {
6 // ブロック (3つ下の行から始まるもの) の外側のライフタイムを 'outer とします。
7 let mut x: i32 = 3;
8 let rx = &mut x;
9 { // ブロック開始、この内部のライフタイムを 'inner とします。
10 let mut y = 4;
11 let ry = &mut y;
12 { // さらにこの内部のライフタイムを適当に 'center とでもします。
13 let rry = foo(rx, ry);
14 // NLL/MIR-borrowckが有効のときはrryはここまで生存する
15
16 // error: use of moved value: `rx`
17 // println!("{}", rx);
18
19 // error: cannot borrow `ry` as immutable because `*ry` is also borrowed as mutable
20 // (と言って foo() の呼び出し箇所を指す)
21 println!("{}", ry);
22 } // NLL/MIR-borrowckが無効のとき(AST-borrowck)はrryはここまで生存する
23 }
24}
これの原理ですが、AST-borrowckのリージョンはrustc::middle::region::ScopeData
で定義されており、端的に言うと以下のようになります。
- 関数より長いライフタイム (名前のついたライフタイムや
'static
)
- 式に対応するライフタイム
- ブロックのある文以降に対応するライフタイム
ここで「ブロックのある場所からある場所まで」という範囲はリージョンではないので、AST-borrowckでは、rryの考えられる最小の生存期間は「rryが定義されてから、ブロックの終わりまで」となります。
NLL/MIR-borrowckの場合のリージョンの扱いについては、NLL RFCの冒頭では「制御グラフ上の点の集まり」と説明されています。ですから少なくとも上記のようなブロックのある区間くらいは扱えると考えられます。
さて、AST-borrowckではrryが 'center
の間は少なくとも生存しなければいけないので、当然そこに入っている参照も 'center
の間生存する必要があります。したがって foo
の2つの引数にも同様の制約が課されることになります。
ライフタイムの大小が一見おかしい理由
さて、では再借用されていないのにライフタイムが縮まる問題ですが、引数の型推論の処理に答えがありそうです。ここを見ると、
- まず、仮引数と実引数の型を比べて、条件次第で型強制を挿入する。
- ・ちなみにコンパイラ内部では再借用は型強制の一種として処理されています。
- ・型強制のコードを見るとわかりますが、強制先の型が参照と判明しているときだけ再借用が検討されます。これが、ジェネリックパラメーターでは再借用されないとされる所以です。
- ↑の成否にかかわらず、実引数が仮引数のサブタイプであるという制約を追加する。
となっています。今回は rx
は再借用を免れてはいますが、サブタイプされてより小さいライフタイムとみなされているのではないかと思います。
なお、こういう事情がありますから、基本的に再借用が抑制されて嬉しいパターンはあまり多くないです。ぼくが知っている唯一の例は連結リストの例です。
追記: サブタイプの型推論について
「実引数が仮引数のサブタイプであるという制約を追加する。」についてより詳しく説明します。結論からいうと、これをした時点で T
が &mut i32
の形であること、そしてそのライフタイムがもとのライフタイムより同じか小さいことが確定します。
まず、Rustの型推論は単相Hindley-Milnerに基づいているので、軽く復習しておきます。Niko Matsakis氏のブログでも使われている(比較的一般的と思われる)記法として、型変数を ?T
と表記することにします。
Hindley-Milnerでは、 type1 == type2
という制約が追加されるごとに、現時点でわかっている最も一般的な解(most general unifier; mgu)を求めていきます。
たとえば、?T
, ?U
, ?V
が未解決の型変数として、 (Vec<?T>, Option<?U>) == (?U, Option<?V>)
という制約が追加されると、
(Vec<?T>, Option<?U>) == (?U, Option<?V>)
⇔ Vec<?T> == ?U
かつ Option<?U> == Option<?V>
なのでこれらを再帰的に解く
Vec<?T> == ?U
なので ?U
に Vec<?T>
を代入する
Option<?U> == Option<?V>
⇔ Option<Vec<?T>> == Option<?V>
⇔ Vec<?T> == ?V
なので ?V
に Vec<?T>
を代入する
というようにして、「?U
に Vec<?T>
を代入し、 ?V
に Vec<?T>
を代入する」のが最も一般的な解であることがわかります。 (?T
の中身は依然不明なので、その後追加される制約で解決されることが期待されます。)
最も基本的なHindley-Milnerでは以上のようにして等号を再帰的に解きます。ではRustのサブタイピングが入った場合のHindley-Milnerを考えてみます。
この場合、 type1 == type2
の形の制約に加えて、 subtype <: supertype
の形の制約を考える必要があります。しかしやることは同じで、一般性を失わないように制約を分解していけばいいことになります。
では &'a mut i32 <: ?T
という制約の場合はどうでしょうか。まず、&mut
は組み込みの構文が与えられていますが、型システムという観点からは RefMut<'a, T>
のような型とみなせます。 (標準ライブラリの同名の型とは別です) つまり、 RefMut<'a, i32> <: ?T
という制約を解くことになります。
この制約から確実にわかることはなんでしょうか。Rustのサブタイピングでは生存期間以外の構造が変わることはありません。したがって ?T
が RefMut
であることはこの時点でわかっています。つまり、新しい生存期間変数 'b
と型変数 ?U
を導入して
RefMut<'a, i32> <: RefMut<'b, ?U>
かつ RefMut<'b, ?U> == ?T
と書けることになります。あとは RefMut
同士のサブタイプ制約を分解するだけです。 RefMut<'a, T>
は 'a
に対して共変で T
に対して非変ですから、
RefMut<'a, i32> <: RefMut<'b, ?U>
⇔ 'a <: 'b
かつ i32 == ?U
となります。 'a <: 'b
(期間の包含でいうと 'b <= 'a
) は型推論にとってはもう分解できない制約なので(そのままborrow checkerに渡される)、これで終わりです。結局、
- 新しい変数
'b
, ?U
を導入する
?T == RefMut<'a, ?U>
?U == i32
'a <: 'b
とするのが、この時点で最も一般的な解ということになります。
これが、前の節の「実引数が仮引数のサブタイプであるという制約を追加する。」の中で起こっていることです。
おまけ
ところで foo()
に渡す順序を変えて foo(ry, rx)
とすると、今度は use of moved value: 'ry'
となりました。あまり考えず、第一引数に合わせて推論しているような気もします。
Rustコンパイラでは基本的に、ソースコード上の順番と各種トラバーサルの順番(型推論も含む)、そして評価順序ができるだけ一致することが求められています。(たしかvisitor/folderまわりのコードにそれを示唆するコメントがあったはず) この場合もそれにのっとって、
- 第一引数のcoercion: この時点では
T
が不明のため再借用は起こらない。
- 第一引数のsubtyping: この時点でunificationが起こって
T
が参照とわかる。
- 第二引数のcoercion: この時点ですでに
T
が参照とわかっているため再借用が起こる。
という順番で処理が進んでいるのだと思います。
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2018/07/14 16:02
2018/07/15 02:25
2018/07/15 02:38
2018/07/15 08:56
2018/07/15 10:46
2018/07/15 12:40
2018/07/15 12:52