質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
Rust

Rustは、MoFoが支援するプログラミング言語。高速性を維持しつつも、メモリ管理を安全に行うことが可能な言語です。同じコンパイル言語であるC言語やC++では困難だったマルチスレッドを実装しやすく、並行性という点においても優れています。

Q&A

解決済

1回答

4137閲覧

ジェネリクスの型推論と再借用について

Eki

総合スコア429

Rust

Rustは、MoFoが支援するプログラミング言語。高速性を維持しつつも、メモリ管理を安全に行うことが可能な言語です。同じコンパイル言語であるC言語やC++では困難だったマルチスレッドを実装しやすく、並行性という点においても優れています。

3グッド

3クリップ

投稿2018/07/13 17:36

編集2018/07/14 10:58

Rust の再借用のしくみについての質問です。

Rust では、 rx: &mut T を関数に渡したり別の変数に let 束縛したりする際、型のライフタイムが合わなければ自動的に &mut *rx 様に再借用をするしくみがありますが、ジェネリクスの型推論や型を省略した let など、ライフタイムを含めて厳密に型が一致するときは、 &mut TCopy を実装していないことにより、その変数にムーブされる挙動をしますよね。

そこで、関数 foo() を次のように定義します。

rust

1fn foo<T>(_x: T, y: T) -> T { y }

これを次のように使うと、コメントしてある通りのエラーになります。
なお、コメントの通りライフタイムに名前を付けます。

修正 少し勘違いして間違ったことを書いていたので、編集させていただきました。これ以下は修正したものです。質問の趣旨は変わりません。また、その後の具体例があまり意味をなしていなかったので削除しました。


x の型を表すのに decltype(x) という表記を借りてくることにします。型同士の不等号はその型の生存期間の長さを表すものとします (サブタイピング的な意味ではありません) 。

rust

1// ブロック (3つ下の行から始まるもの) の外側のライフタイムを 'outer とします。 2let mut x: i32 = 3; 3let rx = &mut x; 4{ // ブロック開始、この内部のライフタイムを 'inner とします。 5 let mut y = 4; 6 let ry = &mut y; 7 { // さらにこの内部のライフタイムを適当に 'center とでもします。 8 let rry = foo(rx, ry); 9 10 // error: use of moved value: `rx` 11 // println!("{}", rx); 12 13 // error: cannot borrow `ry` as immutable because `*ry` is also borrowed as mutable 14 // (と言って foo() の呼び出し箇所を指す) 15 // println!("{}", ry); 16 } 17}
  1. エラーは rx がムーブされ、 ry が再借用されていることを表すので、 T == decltype(rx) かつ T != decltype(ry) となります。
  2. ry の再借用がなされるからには、T の生存期間は y の生存期間 'inner と同じかそれより短い必要、つまり、T <= 'inner となる必要があるはずです。
  3. ry を表示する println! が件のエラーを出すからには、 println! 実行時点で参照 rry が有効である必要、つまり T >= 'center となる必要があります。

以上から decltype(rx) == T == &'center mut i32 で、 decltype(ry) == &'inner mut i32 ということになります。これはしかし不自然です。普通は rx の方が外で宣言されているので長く生き延びるはずですし、必要最低限の期間に絞られているとしたら ry もやはり絞られているべきと考えるからです。

修正ここまで

長くなりましたが、読んでいただきありがとうございます。よろしくお願いします。


ところで foo() に渡す順序を変えて foo(ry, rx) とすると、今度は use of moved value: 'ry' となりました。あまり考えず、第一引数に合わせて推論しているような気もします。

qnighy, yohhoy, kngwyu👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

ベストアンサー

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 なので ?UVec<?T> を代入する
    • Option<?U> == Option<?V>Option<Vec<?T>> == Option<?V>Vec<?T> == ?V なので ?VVec<?T> を代入する

というようにして、「?UVec<?T> を代入し、 ?VVec<?T> を代入する」のが最も一般的な解であることがわかります。 (?T の中身は依然不明なので、その後追加される制約で解決されることが期待されます。)

最も基本的なHindley-Milnerでは以上のようにして等号を再帰的に解きます。ではRustのサブタイピングが入った場合のHindley-Milnerを考えてみます。

この場合、 type1 == type2 の形の制約に加えて、 subtype <: supertype の形の制約を考える必要があります。しかしやることは同じで、一般性を失わないように制約を分解していけばいいことになります。

では &'a mut i32 <: ?T という制約の場合はどうでしょうか。まず、&mutは組み込みの構文が与えられていますが、型システムという観点からは RefMut<'a, T> のような型とみなせます。 (標準ライブラリの同名の型とは別です) つまり、 RefMut<'a, i32> <: ?T という制約を解くことになります。

この制約から確実にわかることはなんでしょうか。Rustのサブタイピングでは生存期間以外の構造が変わることはありません。したがって ?TRefMut であることはこの時点でわかっています。つまり、新しい生存期間変数 '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 13:29

編集2018/07/15 02:05
qnighy

総合スコア210

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Eki

2018/07/14 16:02

いつも回答していただきありがとうございます。 とりあえず `rry` の生存期間に関しては理解ができました。今回の場合、 NLL でない場合は、基本的にスコープの終わりまでがライフタイムとみなされるため、今回は `println!()` の呼び出しまで生存するということですね。 そして、ライフタイムの大小に関してなのですが、結論としては `T` は `&'center mut i32` ということでよいのでしょうか。 > ↑の成否にかかわらず、実引数が仮引数のサブタイプであるという制約を追加する。 ここでいう「制約」というのは、型引数 `T` が満たすべき条件ということですか。実引数 `rx`, `ry` が仮引数 `_x`, `y` のサブタイプである (`rx`, `ry` の生存期間の長さは `_x`, `y` の生存期間よりも長い) ということでよいのでしょうか。 そうだとするならば、第二引数の推論にあたって、まだ `T` の情報を集めているということになりますね (第一引数を見てすぐ `T == decltype(rx)` と決定してしまうわけではないのですね) 。第一引数を見終わった段階で `T` はどういう情報を持っているのでしょう。ライフタイム以外分かっているのか (`&'??? mut i32`) 、参照ということしか分かっていないのか...? とりあえず、コードを前から見て「先に `rx` を再借用 (型強制) しない」という判断をしてから、「実引数の型は仮引数の型 `T` のサブタイプでなければならない」という条件を見たせるよう `T` のライフタイムを調整しているので、外から結果だけ見ると型が違うのに再借用がされていない状態になってしまっている、という理解をしましたが、問題ないでしょうか。
qnighy

2018/07/15 02:25

> そして、ライフタイムの大小に関してなのですが、結論としては `T` は `&'center mut i32` ということでよいのでしょうか。 現在のコードで、条件を見たす最も小さいリージョンは `'center` なので、それでよいと思います。 > 第一引数を見終わった段階で `T` はどういう情報を持っているのでしょう。ライフタイム以外分かっているのか (`&'??? mut i32`) 、参照ということしか分かっていないのか...? `&mut i32` であることと、そのライフタイムがもとのライフタイムより同じか小さいことがわかっていると考えられます。これについて追記しました。 > とりあえず、コードを前から見て「先に `rx` を再借用 (型強制) しない」という判断をしてから、「実引数の型は仮引数の型 `T` のサブタイプでなければならない」という条件を見たせるよう `T` のライフタイムを調整しているので、外から結果だけ見ると型が違うのに再借用がされていない状態になってしまっている、という理解をしましたが、問題ないでしょうか。 これはそもそも、質問文にあったような > 型のライフタイムが合わなければ自動的に `&mut *rx` 様に再借用をするしくみ というのが不正確で、型のライフタイムが合っているかどうかを特別に見ているわけではなさそうです。単に場合によってアドホックに再借用をしたりしなかったりしていて (そのより具体的なルールは回答した通りです) あまり深い意図はない気がします。 またRustの型推論ではサブタイピング制約をダイレクトに解くので、「型の構造が違う(例: `&Box<i32>`と`&i32`)」のには敏感ですが(これをトリガーにして型強制をする)、「ライフタイムが違う(例: `&'a i32`と`&'b i32`)」のは普通のことです。 普通は「実引数と仮引数は同じ型」として問題を解くと思いますが、Rustでははなからそのようには考えず、それより若干ゆるい「実引数の型は仮引数の型のサブタイプ」という問題を解いているということです。
Eki

2018/07/15 08:56

ありがとうございます。ようやくいろいろな疑問が解決しました。 まず、型推論についての丁寧な説明をありがとうございます。とても分かりやすかったです。 `T` に何が起こっていたかということが分かりました。 また、 > これはそもそも、質問文にあったような ... というのが不正確 についても理解できました。実際に次のコードを試してみたところ、 ```rust #![allow(unused_variables, unused_assignments)] fn main() { let mut x = 4; let (r, rr): (&mut i32, &mut i32); r = &mut x; rr = r; // error: cannot borrow `r` as immutable because `*r` is also borrowed as mutable // println!("{}", r); } ``` コメントのようなエラーになりました。ここでは `r` と `rr` の型はライフタイムを含めて同一ということで良いですよね。なのにエラーメッセージを見ていると `r` はどうも再借用されているらしいことがわかります。ということで、ライフタイムに関係なく、参照が必要と分かっている場所では再借用が起こるという仕組みであることに納得できました。 (ということで大丈夫ですか...?) --- もとの疑問は氷解したのですが、もう一つだけ気になってきました。現在の第一引数をムーブしてしまう挙動は積極的に意図された挙動なのでしょうか? `&mut` を再利用できるようにしたいという思いがあって、再借用というルールが実装されているということでしたよね。それは確かに納得しました。ただ、その意図を実装したものとしては、今の実装は少なくとも第一引数をムーブしてしまっている時点で少し則していないと感じます。これは、 * 本当は第一引数も参照なのだから再借用されて欲しい。が、実装の都合 (再借用判定フェーズ=型強制の段階では参照と判明していないこと) があって難しい (またはコードが複雑になりすぎてコスパが悪い) ため、このままにしてある。 * 積極的にこの挙動が欲しいときがある。 のどちらなのでしょう。 `&mut` をムーブして嬉しい例は私も連結リストを辿る例しか知らないのですが、これはこの挙動に頼らずとも {} ブロックを使うことで同様の挙動にできたはずです。
qnighy

2018/07/15 10:46

> ここでは `r` と `rr` の型はライフタイムを含めて同一ということで良いですよね。 それを確かめるなら次のようにする必要があると思います。(結果的には同じ出力になるので、結論自体は合っていると思います。) http://play.rust-lang.org/?gist=daf317113cfe5a1a6f2c6cfcb70d4388&version=stable&mode=debug&edition=2015 > いうことで、ライフタイムに関係なく、参照が必要と分かっている場所では再借用が起こるという仕組みであることに納得できました。 (ということで大丈夫ですか...?) それでいいと思います。 > 現在の第一引数をムーブしてしまう挙動は積極的に意図された挙動なのでしょうか? これはぼくも疑問に思っているところです。実のところトレイト選択や型強制・メソッド解決まわりは、コンパイラの作業順がそのまま仕様として露出してしまっている箇所が少なからずあると感じています。なので、単に当時そのような仕様で実装されたものが今に至るまで維持されてしまっただけ、という可能性もあると思っています。真実を知るには頑張って歴史を掘り起こすか、当時を知る人に聞くしかないのかもしれません。 > これはこの挙動に頼らずとも {} ブロックを使うことで同様の挙動にできたはずです。 `{}` ブロックがそのような挙動をもたらすのも元はといえば同じ型強制の仕様に由来すると思うので、そう単純な話でもないかなと思っています。このあたりは型のexpectationというアドホックな仕組みのおかげでさらに複雑化していて、ぼくも全体像を把握できてないのですが……
Eki

2018/07/15 12:40

たしかに。タプルだと別に型が異なっていてもいいわけですからね... この場合 rr の生存期間は rr より長ければ何でもいいわけですもんね。 固定長配列もパターンで分解できるのは、言われてみればできるはずだけどまったく知りませんでした。 そのあたりの問題はすごく面白いです。 確かに参照と分かっていたら {} しても再借用されますね (let で試してみました) 。let rr = r; でムーブするのも冷静に考えたら同じ仕組みになりそうですし、逆に一切ムーブする方法がなくなるというのもそれはそれで困るわけですしね (連結リストの例) 。 今回もよく分かりました。おつきあい頂き、ありがとうございました。
qnighy

2018/07/15 12:52

今回も面白いネタをありがとうございました。 > 固定長配列もパターンで分解できるのは、言われてみればできるはずだけどまったく知りませんでした。 スライスパターンはまだ議論が残っていて、固定長配列パターンが安定化されたのも結構最近です。 https://github.com/rust-lang/rust/blob/1.27.1/RELEASES.md#language-1
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問