不透明型(impl Trait)の推論
このページでは、コンパイラが不透明型の隠れた型をどのように推論するかを説明します。 この種の型推論は特に複雑です。 なぜなら、他の種類の型推論とは異なり、 関数や関数本体をまたいで機能できるためです。
実行例
仕組みを説明するために、例を考えてみましょう。
#![feature(type_alias_impl_trait)]
mod m {
pub type Seq<T> = impl IntoIterator<Item = T>;
#[define_opaque(Seq)]
pub fn produce_singleton<T>(t: T) -> Seq<T> {
vec![t]
}
#[define_opaque(Seq)]
pub fn produce_doubleton<T>(t: T, u: T) -> Seq<T> {
vec![t, u]
}
}
fn is_send<T: Send>(_: &T) {}
pub fn main() {
let elems = m::produce_singleton(22);
is_send(&elems);
for elem in elems {
println!("elem = {:?}", elem);
}
}
このコードでは、不透明型は Seq<T> です。
その定義スコープはモジュール m です。
その隠れた型は Vec<T> で、
これは m::produce_singleton と m::produce_doubleton から推論されます。
main 関数では、不透明型はその定義スコープの外にあります。
main が m::produce_singleton を呼び出すと、不透明型 Seq<i32> への参照が返されます。
is_send 呼び出しは Seq<i32>: Send であることを検査します。
Send は impl trait の境界には列挙されていませんが、
auto-trait leakage により、それが成り立つことを推論できます。
for ループの脱糖では Seq<T>: IntoIterator であることが必要で、
これは Seq<T> に宣言された境界から証明できます。
main の型チェック
まず、main を型チェックするときに何が起こるかを見てみましょう。
最初に produce_singleton を呼び出し、戻り値の型は不透明型
OpaqueTy になります。
for ループの型チェック
for ループは、in elems の部分を IntoIterator::into_iter(elems) に脱糖します。
elems の型は Seq<T> なので、型チェッカーは Seq<T>: IntoIterator 義務を登録します。
この義務は自明に満たされます。
なぜなら、Seq<T> はそのトレイトに対する境界を持つ不透明型(impl IntoIterator<Item = T>)だからです。
U: Foo という where 境界によって U が自明に Foo を満たせるのと同様に、
不透明型の境界は型チェッカーから利用可能であり、義務を満たすために使われます。
for ループ内の elem の型は <Seq<T> as IntoIterator>::Item、つまり T と推論されます。
型チェッカーが隠れた型に関心を持つことは一切ありません。
is_send 呼び出しの型チェック
auto trait 境界を証明しようとするとき、
まず上記と同じプロセスを繰り返し、
その auto trait が不透明型の境界リストに含まれているかを確認します。
それが失敗した場合、不透明型の隠れた型を明らかにしますが、
これはこの特定のトレイト境界を証明するためだけであり、一般的に明らかにするわけではありません。
明らかにする処理は、不透明型の DefId に対して type_of クエリを呼び出すことで行われます。
クエリは内部で、定義関数から隠れた型を要求し、
それを返します(詳細は type_of に関するセクションを参照してください)。
型チェック手順のフローチャート
flowchart TD
TypeChecking["`main` の型チェック"]
subgraph TypeOfSeq["type_of(Seq<T>) クエリ"]
WalkModuleHir["モジュール `m` の HIR をたどり、\nその中にある各 function/const/static から\n隠れた型を見つける"]
VisitProduceSingleton["`produce_singleton` を訪問"]
InterimType["`produce_singleton` の隠れた型は `Vec<T>`\n検索を続ける"]
VisitProduceDoubleton["`produce_doubleton` を訪問"]
CompareType["`produce_doubleton` の隠れた型も Vec<T>\nこれは以前に見たものと一致する ✅"]
Done["スコープ内に見るべきアイテムはもうない\n`Vec<T>` を返す"]
end
BorrowCheckProduceSingleton["`borrow_check(produce_singleton)`"]
TypeCheckProduceSingleton["`type_check(produce_singleton)`"]
BorrowCheckProduceDoubleton["`borrow_check(produce_doubleton)`"]
TypeCheckProduceDoubleton["`type_check(produce_doubleton)`"]
Substitute["`T => u32` を代入し、\n隠れた型として `Vec<i32>` を得る"]
CheckSend["`Vec<i32>: Send` であることを検査 ✅"]
TypeChecking -- auto trait 用のトレイトコード --> TypeOfSeq
TypeOfSeq --> WalkModuleHir
WalkModuleHir --> VisitProduceSingleton
VisitProduceSingleton --> BorrowCheckProduceSingleton
BorrowCheckProduceSingleton --> TypeCheckProduceSingleton
TypeCheckProduceSingleton --> InterimType
InterimType --> VisitProduceDoubleton
VisitProduceDoubleton --> BorrowCheckProduceDoubleton
BorrowCheckProduceDoubleton --> TypeCheckProduceDoubleton
TypeCheckProduceDoubleton --> CompareType --> Done
Done --> Substitute --> CheckSend
type_of クエリの内部
不透明型 O に適用された type_of クエリは、隠れた型を返します。
その隠れた型は、O の定義スコープ内にある各制約関数からの結果を
組み合わせることで計算されます。
flowchart TD
TypeOf["type_of クエリ"]
TypeOf -- find_opaque_ty_constraints --> FindOpaqueTyConstraints
FindOpaqueTyConstraints --> Iterate
Iterate["定義スコープ内の各アイテムを反復処理する"]
Iterate -- 各アイテムについて --> TypeCheck
TypeCheck["typeck(I) を検査して、それが O を制約するか確認する"]
TypeCheck -- I は O を\n制約しない --> Iterate
TypeCheck -- I は O を制約する --> BorrowCheck
BorrowCheck["mir_borrowck(I) を呼び出し、I によって計算された\nO の隠れた型を取得する"]
BorrowCheck --> PreviousType
PreviousType["I からの隠れた型は\nこれまでに見つかった以前の隠れた型と\n同じか?"]
PreviousType -- はい --> Complete
PreviousType -- いいえ --> ReportError
ReportError["エラーを報告する"]
ReportError --> Complete["アイテム I 完了"]
Complete --> Iterate
FindOpaqueTyConstraints -- すべての制約が見つかった --> Done
Done["完了"]
不透明型を別の型に関連付ける
不透明型がその隠れた型によって制約される中心的な場所が 1 つあり、
それが handle_opaque_type 関数です。
面白いことに、この関数は 2 つの型を受け取るので、任意の 2 つの型を渡せますが、
そのうちの 1 つは不透明型である必要があります。
順序が重要なのは診断に対してのみです。
flowchart TD
subgraph typecheck["type check comparison routines"]
equate.rs
sub.rs
lub.rs
end
typecheck --> TwoSimul
subgraph handleopaquetype["infcx.handle_opaque_type"]
TwoSimul["Defining two opaque types simultaneously?"]
TwoSimul -- Yes --> ReportError["Report error"]
TwoSimul -- No --> MayDefine -- Yes --> RegisterOpaqueType --> AlreadyHasValue
MayDefine -- No --> ReportError
MayDefine["In defining scope OR in query?"]
AlreadyHasValue["Opaque type X already has\na registered value?"]
AlreadyHasValue -- No --> Obligations["Register opaque type bounds\nas obligations for hidden type"]
RegisterOpaqueType["Register opaque type with\nother type as value"]
AlreadyHasValue -- Yes --> EquateOpaqueTypes["Equate new hidden type\nwith old hidden type"]
end
クエリとの相互作用
クエリが opaque 型を処理するとき、 それらは自分が定義スコープ内にいるかどうかを判断できないため、 単にそうであると仮定します。
登録された隠れた型は、QueryResponse 構造体の
opaque_types フィールドに格納されます(関数
take_opaque_types_for_query_response がそれらを読み出します)。
QueryResponse が query_response_substitution_guess で
周囲の infcx にインスタンス化されるとき、
各隠れた型の制約を、(上記のように)handle_opaque_type を呼び出すことで変換します。
「奇妙」な点が 1 つあります。 インスタンス化された opaque 型には順序があります (ある opaque 型が別の opaque 型と比較され、 どちらの opaque 型を、その隠れた型が割り当てられるものとして使うかを選ばなければならない場合)。 「期待される」と見なされるものを使用します。 しかし実際には、両方の opaque 型に定義使用がある可能性があります。 クエリ結果がインスタンス化されると、 そのクエリを使用しているコンテキストから再評価されます。 最終的なコンテキスト(関数の typeck、mir borrowck、または wf-checks)は、 どの opaque 型を実際にインスタンス化できるかを把握し、 それを正しく処理します。
MIR 借用チェッカー内
MIR 借用チェッカーは nll_relate を介して物事を関連付け、リージョンだけを考慮します。
あらゆる型関係は隠れた型の束縛をトリガーするため、
借用チェッカーは型チェッカーと同じことをしていますが、
明らかに到達不能なコード(例: panic の後)は無視します。
また、隠れた型に関しては借用チェッカーが信頼できる情報源でもあります。
なぜなら、隠れた型上のどのライフタイムが
opaque 型宣言上のどのライフタイムに対応するかを適切に把握できるのは、
借用チェッカーだけだからです。
後方互換性ハック
戻り値位置の impl Trait には、どの RFC にも含まれておらず、
偶発的に安定化された可能性が高いさまざまな癖があります。
これらをサポートするために、
以前の挙動を再導入する目的で replace_opaque_types_with_inference_vars が使われています。
後方互換性ハックは 3 つあります。
-
すべての return 箇所が同じ推論変数を共有するため、 ある return 箇所は、別の return 箇所が具象型を使っている場合にのみコンパイルできることがあります。
#![allow(unused)] fn main() { fn foo() -> impl Debug { if false { return std::iter::empty().collect(); } vec![42] } } -
impl Traitの関連型等価制約は、 隠れた型が関連型上のトレイト境界を満たしている限り使用できます。 opaque なimpl Traitシグネチャは、それらを満たす必要はありません。#![allow(unused)] fn main() { trait Duh {} impl Duh for i32 {} trait Trait { type Assoc: Duh; } // `R` が `F` 上の `::Output` 射影であるという事実により、 // 中間の推論変数が生成され、その後、実際に見つかった // `Assoc` 型と比較されます。 impl<R: Duh, F: FnMut() -> R> Trait for F { type Assoc = R; } // ここでの `impl Send` は、後で作成された推論変数と比較され、 // 推論変数が隠れた型ではなく `impl Send` に設定されます。 // 推論変数には、`Trait::Assoc` 上の `: Duh` 境界を守らせるための // obligation がすでに登録されています。opaque 型は、その隠れた型が // `Duh` を実装している場合でも、`Duh` を実装しません。 // Lazy TAIT ではエラーになりますが、再び動作するようにするためにハックを挿入し、 // 後方互換性を維持しました。 fn foo() -> impl Trait<Assoc = impl Send> { || 42 } } -
クロージャは、親関数の
impl Traitの隠れた型を作成できません。 この点はほとんど問題になりません。 なぜなら、1 の点によって推論変数が導入されるため、 クロージャが見るのは推論変数だけだからです。ただし、1 を修正した場合、これは問題になります。