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

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

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

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

Q&A

解決済

1回答

3686閲覧

impl Trait でトレイトを use しなければいけない理由

Eki

総合スコア429

Rust

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

1グッド

1クリップ

投稿2019/07/08 05:10

編集2019/07/08 05:24

以下のようになるのはなぜでしょうか。 (playground)

rust

1fn foo<T: std::io::Write>(mut x: T) { 2 // OK 3 x.write_all(b"hello\n").unwrap(); 4 T::write_all(&mut x, b"Hello").unwrap(); 5} 6 7fn bar_impl<T: std::io::Write>(x: T) -> impl std::io::Write { 8 x 9} 10 11fn bar_dyn<T: std::io::Write + 'static>(x: T) -> Box<dyn std::io::Write> { 12 Box::new(x) 13} 14 15fn main() { 16 foo(std::io::stdout()); 17 18 // OK 19 bar_dyn(std::io::stdout()).write_all(b"world").unwrap(); 20 21 // E0599: no method named `write_all` found for 22 // type `impl std::io::Write` in the current scope 23 bar_impl(std::io::stdout()).write_all(b"hello").unwrap(); 24}

Rust では、トレイトに定義されているメソッドを (Fully Qualified Path を使わず) 呼び出す際は基本的にそのトレイトが use によってスコープに持ち込まれている必要があります。ところがこれにはいくつか例外があるように見えます。上の例では、境界にトレイトが指定されているような型の変数とトレイトオブジェクト Box<dyn Trait> については use せずに使えることが示されています。なぜ impl Trait についてはこうならないのでしょうか。

追記

トレイト境界が明記してあっても単純 (?) なものでないとダメなようです。

rust

1// Error 2fn foo<'a, T, F: FnOnce(&'a mut T) -> &'a mut T>(x: &'a mut T, f: F) 3where 4 &'a mut T: std::io::Read, 5{ 6 let _ = f(x).bytes(); 7} 8 9// OK 10fn bar<'a, T, U, F: FnOnce(&'a mut T) -> U>(x: &'a mut T, f: F) 11where 12 U: std::io::Read, 13{ 14 let _ = f(x).bytes(); 15}
equal-l2👍を押しています

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

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

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

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

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

guest

回答1

0

ベストアンサー

まず、Referencerustc-guideの該当項目ではごく基本的なことしか触れられていませんね。つまり、メソッド解決は以下のように行われることになっています。

  • レシーバーの本来の型を基準に、deref coercion/unsized coercionを適用して取得可能な型を列挙する。
  • 本来の型に近いものから順に、以下のいずれかの候補からメソッド名を探す。
      1. その型の固有メソッド。
      1. 可視なトレイトのメソッド(その型がそのトレイトを実装している場合のみ)。

Ekiさんの疑問はこれよりもさらに踏み込んだものになっているので、ソースコードを読んでみます。まず以下のenumが実質的な答えになっていそうです。

rust

1#[derive(Debug)] 2enum CandidateKind<'tcx> { 3 InherentImplCandidate(SubstsRef<'tcx>, 4 // Normalize obligations 5 Vec<traits::PredicateObligation<'tcx>>), 6 ObjectCandidate, 7 TraitCandidate(ty::TraitRef<'tcx>), 8 WhereClauseCandidate(// Trait 9 ty::PolyTraitRef<'tcx>), 10}

通常の説明で言及されているのは以下の2つですね。

  • InherentImplCandidate。これは所望の型に対する固有実装を列挙して、その中のメソッドで型引数が条件を満たしているものを探しています。
  • TraitCandidate。これは self.tcx.in_scope_traits(expr_hir_id) のメソッドで条件を満たしているものを探しています。

残るは ObjectCandidateWhereClauseCandidate ですが、実際にソースコードを読んでみると当たりのようです。

また ty::Opaque への言及がないことから、 impl Trait の特別扱いはなさそうです。

以上のことからEkiさんの実験結果は説明がつきます。つまり、メソッドの探索範囲は(現状の実装では)実際には以下の4つです。

  • その型に対する impl MyType { ... }
  • その型と、スコープ内のトレイトに対する impl TraitInScope for MyType { ... }
  • dyn MyTrait 型に対しては、 trait MyTrait { ... } も含む
  • 型引数 T に対して T: MyTrait がついていた場合は、 trait MyTrait { ... } も含む

&'a mut T: std::io::Read から探索されなかったのは、この場合のターゲット型が &'a mut T で、型引数型そのものではないからだと説明がつきます。

では、そのように実装された理由はなんでしょうか? これはパッと根拠になる資料が見つからなかったのでちょっとわかりません。かわりに推測を書きます。

  • 一般論として、トレイトメソッドがスコープ内に限られるのは、「無関係なトレイトが足されるだけでメソッド推論が壊れる」というのを極力防止するためだと考えられます。dyn Traitdyn Trait 型自身がトレイトを限定しているし、型引数も T: MyTrait というboundの数だけしか候補は存在しないので、任意にtraitを注入できる構造になっていません。そのため、利便性も考慮して追加の仕様が存在するのでしょう。
  • 型引数型ではないものにtrait boundがついていても推論されないことについては、そういったユーザー体験上の問題というよりは、考えることを減らしたりコンパイラを効率的にするためという要素が強いのではないかと思います。あるいは何も考えてないだけかもしれません。
  • impl Trait については不明です。RFC1522とそのPR上の議論をざっと眺めた限りではわかりませんでした。単に誰も気にしてなかったために実装されなかったとかかもしれません。

投稿2019/07/08 15:54

qnighy

総合スコア210

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

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

Eki

2019/07/09 18:14

なるほど、今回も丁寧な説明をありがとうございます。とりあえずコードの雰囲気はつかむことができました。 impl Trait はある意味忘れられているみたいな感じなのですね...。 少し外れますが、こういうふうにコンパイラのソースを追うときはどのようにされているのですか?ローカルに持ってきたソースを検索するとか、コンパイラのクレートのドキュメントを使うとか、あるいは GitHub をそのまま使うとか...? qnighy さんはなんとなくどのステップがどのへんのファイルにあるのか大雑把に把握されているのかもしれませんが......。
qnighy

2019/07/10 14:29

ぼくがコンパイラを読んでいた当初はあまりいい資料がなかったので、わざとコンパイルエラーを起こしてそのメッセージで検索したり、主要なデータ構造を見つけた上でそのデータの流れを追っていました。今はreferenceとrustc-guideが非常によくできているので、まずそれを読んだほうがいいと思います。 一応ここにも軽く書いておくと、おおよそ ・`src/libsyntax*` と `src/librustc*` を探せばコンパイラ本体はおよそ網羅できる ・`src/libcore`, `src/liballoc`, `src/libstd` を探せば標準ライブラリはおよそ網羅できる ・ASTを生成して構文拡張(bangマクロ、属性マクロ、deriveマクロ)を展開するまではlibsyntax, 残りはlibrustcの仕事 ・librustcは、共通定義が `src/librustc` に書いてあって、個別実装が `src/librustc_*` にあり、それらを纏めて全体の処理フローが `src/librustc_interface` と `src/librustc_driver` に定義されていて、main関数だけは `src/rustc` に定義されている ・名前解決は `src/librustc_resolve`, `src/librustc_privacy` ・HIRは `src/librustc/hir` ・型まわりは `src/librustc/ty`, `src/librustc/traits`, `src/librustc/infer`, `src/librustc_typeck` ・MIRと借用検査は `src/librustc/mir`, `src/librustc_mir`, `src/librustc_borrowck` ・細かい検査系の処理は `src/librustc/middle`, `src/librustc_passes` ・コード生成は `src/librustc_target`, `src/librustc_codegen_*` ・ASTの定義は `src/libsyntax/ast.rs` ・HIRの定義は `src/librustc/hir/mod.rs` ・型の定義は `src/librustc/ty/sty.rs` ・MIRの定義は `src/librustc/mir/mod.rs` といった感じになっています。
Eki

2019/07/10 15:35

なるほど reference と rustc-guide が便利なんですね。 次回からまずのぞいてみます。丁寧にありがとうございます!
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.50%

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

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

質問する

関連した質問