Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

トレイト内の戻り位置 Impl Trait

トレイト内の戻り位置 impl Trait(RPITIT)は、概念的には(そして #112988 の時点では文字どおり)、トレイトメソッド内の RPIT を、ユーザーが トレイト側または impl 側のどちらでもその GAT を定義しなくてもよい ジェネリック関連型(GAT)へ変換する糖衣構文です。

RPITIT は当初 #101224 で実装されました。これは トレイト内の async fn(AFIT)のサポートを追加したもので、RPITIT の実装は、 以前に RFC 化されていた AFIT の実装の一部として 付随的に得られたためです。その後、RFC 3425 で独立して RFC 化され、 最近 T-lang によって承認されました。

どのように動作するか?

このドキュメントは、主にコンパイルパイプラインに沿って並んでいます:

  1. AST lowering(AST -> HIR)
  2. HIR ty lowering(HIR -> rustc_middle::ty データ型)
  3. typeck

AST lowering

RPITIT の AST lowering は、RPIT の lowering とほぼ同じです。私たちは 引き続きそれらを hir::ItemKind::OpaqueTy として lowering します。 2つの違いは次のとおりです:

不透明型に対して in_trait を記録します。これは、その不透明型が HIR ty lowering や HIR を扱う診断などにおいて RPITIT であることを示します。

不透明型に対して lifetime_mapping を記録します。これについては以下で説明します。

補足: 不透明型のライフタイムの重複

すべての不透明型(RPITIT だけではありません)は、捕捉した ライフタイムを、不透明型にローカルな新しいライフタイムパラメータへ 重複させることになります。これを行う主な理由は、RPIT が、捕捉した 任意の late-bound 引数を「具象化」1 できる、つまり early-bound なものに できる必要があるためです。これは、それらを不透明型のジェネリック引数として使い、 後で隠れた型をインスタンス化できるようにするためです。AST lowering の時点では、 どのライフタイムが early-bound で、どれが late-bound か分からないため、 すべてのライフタイムに対してこれを行います。

RPITIT における主な追加点は、lowering 中に、捕捉されたライフタイムと 対応して重複されたライフタイムとの関係を追加のフィールド OpaqueTy::lifetime_mapping で追跡することです。 このライフタイムの対応付けは、後で predicates_of で、これらの重複された ライフタイムとその元のライフタイムとの等価性を強制する境界を設定し、 これらの GAT を正しく型チェックするために使用します。これについては以下で説明します。

重複なしで lowering できるなら、その方がよいかもしれません。そのためには、 early-bound ライフタイムと late-bound ライフタイムの区別をやめる必要があると 私は考えています。したがって、ジェネリクスにおいて late-bound ライフタイムを考慮する #103448 のような解決策と、さらに impl-trait に関数ライフタイムを継承させる #103449 に類似した PR が必要になるでしょう。

HIR ty lowering

HIR ty lowering における主な変更点は、RPITIT の hir::TyKind::OpaqueDef を 不透明型ではなく射影に lowering することです。このとき、トレイト内の 新しい関連型のために新たに合成された def-id を使用します。次のセクションで、 この def-id を正確にどのように取得するかを説明します。

これは、RPITIT に対して lower_ty を呼び出すたびに、 不透明型ではなく射影が返ってくることを意味します。この射影はその後、 正しい値へ正規化できます。つまり、トレイト内にいる場合は元の不透明型へ、 impl 内にいる場合は RPITIT の推論された型へ正規化されます。

合成された関連型への lowering

query feeding を使用して、メソッド内に現れる RPITIT のために、 トレイト側と impl 側の両方で新しい関連型を合成します。

トレイト内の RPITIT の lowering

tcx.associated_item_def_ids(trait_def_id) が、トレイトのすべての関連型を 集めるためにトレイトに対して呼び出されると、以前のクエリは単に そのトレイトの子である HIR アイテムの def-id を返していました。 #112988 以降は、それに加えて、トレイト内の各メソッドについて、 tcx.associated_types_for_impl_traits_in_associated_fn(trait_method_def_id) が返す def-id を追加します。 これは各トレイトメソッドを走査し、シグネチャに現れる RPITIT をすべて集め、 その後、各 RPITIT に対して associated_type_for_impl_trait_in_trait を呼び出し、それが新しい関連型を合成します。

impl 内の RPITIT の lowering

同様に、impl の HIR アイテムとともに、各 impl メソッドについて、 その impl メソッドに対応する associated_types_for_impl_traits_in_associated_fn をすべて追加します。 これは associated_type_for_impl_trait_in_impl を呼び出し、 対応するトレイトメソッドに由来する各 RPITIT について、関連型定義を合成します。

新しい関連型の合成

query feeding (TyCtxtAt::create_def) を使用して、各 RPITIT の合成 GAT のために新しい def-id を合成します。

ローカルでは、rustc のほとんどのクエリは、値を計算するためにアイテムの HIR に 対してマッチします。RPITIT には実際には関連付けられた HIR がない、 少なくとも関連型に対応する HIR はないため、多くのクエリを先行して計算し、 それらを feed しなければなりません。たとえば opt_def_kindassociated_itemvisibility、および defaultness です。

これらのクエリのほとんどについて、その値は明らかです。RPITIT は概念的に その情報の大半を親関数から継承する(例: visibility)か、 関連型であるため自明に分かる(opt_def_kind)ためです。

その他のいくつかのクエリはより込み入っているか、feed できません。 そのうち興味深いものを以下に記します:

トレイトに対する generics_of

RPITIT の GAT は概念的に、その由来となる RPIT と同じジェネリクスを継承します。 ただし、ジェネリクスの親がメソッドになるのではなく、トレイトが親になります。

現在のところ、RPIT のジェネリクスとメソッドのジェネリクスを取り、 それらをどちらも新しいジェネリクスリストへフラット化し、各パラメータの def-id を保持することで済ませています。(これにより def-id の親が誤っている 問題が発生する可能性がありますが、最悪の場合でも診断の問題を引き起こすだけです。 これが問題になる場合は、親が GAT であるジェネリックパラメータのために 新しい def-id を合成できます。)

図解例
#![allow(unused)]
fn main() {
trait Foo {
    fn method<'early: 'early, 'late, T>() -> impl Sized + Captures<'early, 'late>;
}
}

次のように脱糖されます…

#![allow(unused)]
fn main() {
trait Foo {
    //       vvvvvvvvv メソッドのジェネリクス
    //                  vvvvvvvvvvvvvvvvvvvvvvvv 不透明型のジェネリクス
    type Gat<'early, T, 'early_duplicated, 'late>: Sized + Captures<'early_duplicated, 'late>;

    fn method<'early: 'early, 'late, T>() -> Self::Gat<'early, T, 'early, 'late>;
}
}
impl に対する generics_of

impl の GAT のジェネリクスは、少し興味深いものです。それらは、 RPITIT 自身のジェネリクス(トレイト定義由来)を、 impl のメソッドのジェネリクスに追加したもので構成されます。これには上記と同じ問題があり、 GAT のジェネリクスには def-id の親が誤っているパラメータが含まれますが、 これは診断においてのみ問題を引き起こすはずです。 もし新しいジェネリクスの def-id を合成するなら、同様にこれを修正できますが、これは後から将来互換のある方法で行えます。おそらく、関心を持った新しいコントリビューターによって行われるでしょう。

opt_rpitit_info

一部のクエリは、explicit_predicates_of のように、先行して与えるとサイクルを引き起こす情報の計算に依存しています。 そのため、RPITIT の GAT に対して正しい値を返す処理は predicates_of プロバイダーに委ねます。これは、クエリの早い段階で opt_rpitit_info を使って関連型が合成されたものかどうかを検出することで行います。このメソッドは、関連型が合成されたものである場合に Some を返します。

その後、explicit_predicates_of のようなクエリの中で、関連型が合成されたものかどうかを次のように検出できます。

#![allow(unused)]
fn main() {
fn explicit_predicates_of(tcx: TyCtxt<'_>, def_id: LocalDefId) -> ... {
    if let Some(rpitit_info) = tcx.opt_rpitit_info(def_id) {
        // RPITIT のための特別な処理を行う...
        return ...;
    }

    // `def_id` の HIR へのアクセスに依存する通常の計算。
}
}
explicit_predicates_of

RPITIT は、それを定義したメソッドの述語を、trait 側と impl 側の両方でコピーすることから始まります。

さらに、「双方向の outlives」述語をインストールします。 具体的には、キャプチャされた各 early-bound ライフタイムについて、両方向の region-outlives 述語を追加し、それが lowering によって生じる複製された early-bound ライフタイムと等しくなるように制約します。これは例で示すのが最も分かりやすいです。

#![allow(unused)]
fn main() {
trait Foo<'a> {
    fn bar() -> impl Sized + 'a;
}

// 次のように脱糖される...

trait Foo<'a> {
    type Gat<'a_duplicated>: Sized + 'a
    where
        'a: 'a_duplicated,
        'a_duplicated: 'a;
    //~^ 具体的には、複製された `'a_duplicated` ライフタイムが
    // 常に `'a` ライフタイムと同期していると
    // 仮定できるべきである。

    fn bar() -> Self::Gat<'a>;
}
}
assumed_wf_types

trait と impl の両方にある GAT は、RPITIT を定義する trait メソッドの assumed_wf_types を継承します。これは、次のコードが lowering されたときに well-formed であることを保証するためです。

#![allow(unused)]
fn main() {
trait Foo {
    fn iter<'a, T>(x: &'a [T]) -> impl Iterator<Item = &'a T>;
}

// 次のように lowering される...

trait FooDesugared {
    type Iter<'a, T>: Iterator<Item = &'a T>;
    //~^ 仮定された WF: `&'a [T]`
    // 仮定された WF 型がなければ、GAT はそれ自体では well-formed にならない。

    fn iter<'a, T>(x: &'a [T]) -> Self::Iter<'a, T>;
}
}

assumed_wf_types はローカル def id に対してのみ定義されているため、外部 trait の RPIT を持つ impl に対して assumed_wf_types を適切に実装するには、RPITIT の仮定された WF 型を extern クエリ assumed_wf_types_for_rpitit にエンコードする必要があります。

型検査

RPITIT 推論アルゴリズム

RPITIT 推論アルゴリズムは collect_return_position_impl_trait_in_trait_tys に実装されています。

高レベル: impl メソッドと trait メソッドが与えられると、trait メソッドを取り、そのシグネチャ内の各 RPITIT を推論変数でインスタンス化します。次に、この trait メソッドのシグネチャを impl メソッドのシグネチャと等置し、その結果として発生するすべての obligation を処理して、メソッド内のすべての RPITIT の型を推論します。

このメソッドは、各 RPITIT の hidden type が impl Trait の境界を実際に満たすこと、つまり impl Trait = Foo と推論した場合に Foo: Trait が成り立つことを確認する役割も担います。

例...
#![allow(unused)]
#![feature(return_position_impl_trait_in_trait)]

fn main() {
use std::ops::Deref;

trait Foo {
    fn bar() -> impl Deref<Target = impl Sized>;
             // ^- RPITIT ?0        ^- RPITIT ?1
}

impl Foo for () {
    fn bar() -> Box<String> { Box::new(String::new()) }
}
}

最終的に、fn() -> ?0 のような trait シグネチャと、ネストされた obligation ?0: Deref<Target = ?1>?1: Sized が得られます。impl シグネチャは fn() -> Box<String> です。

これらのシグネチャを等置すると ?0 = Box<String> が得られ、その後 obligation Box<String>: Deref<Target = ?1> を処理すると ?1 = String が得られ、もう一方の obligation String: Sized は true と評価されます。

アルゴリズムの終了時には、関連型の def-id からシグネチャから推論された具象型へのマッピングが得られます。このマッピングは各 RPITIT について type Assoc = ...= の後に来るべき型を記述しているため、これを使って impl 内の合成関連型に対する type_of を実装できます。

RPITIT hidden type 推論における implied bounds

collect_return_position_impl_trait_in_trait_tys は fulfillment とリージョン解決を行うため、compare_method_predicate_entailment と同じ期待される implied bounds によってリージョン obligation を証明できるよう、これに assumed_wf_types を提供しなければなりません。

メソッドの戻り値型は仮定された WF 型の 1 つであると理解されており、さらに opaque type 推論を行うために戻り値型を推論変数で先行して fold するため、opaque type 推論の後、戻り値型は RPITIT の hidden type を含むように解決されます。これは、RPITIT の hidden type が、それ自体が well-formed であることを独立して証明されることなく、well-formed であると仮定されることを意味します。これにより、 微妙な unsoundness バグ が発生しました。この循環的な推論を防ぐため、代わりにメソッドの戻り値型に含まれる RPITIT の hidden type を placeholder に置き換えます。これにより、implied well-formedness bounds は発生しません。

デフォルト trait 本体

次のようなデフォルト trait 本体を型検査するには、

#![allow(unused)]
fn main() {
trait Foo {
    fn bar() -> impl Sized {
        1i32
    }
}
}

興味深いハックが 1 つ必要です。Foo::bar の param-env に projection 述語をインストールし、RPITIT の GAT が RPITIT の opaque type に正規化されると仮定できるようにする必要があります。これは、trait メソッドと RPITIT の GAT が常に「同期している」という観察に依存しています。つまり、一方がオーバーライドされる場合、もう一方も必ずオーバーライドされます。

これを、上のコードの類似した脱糖と比較してください。次のコードは同じ仮定に依存できないため失敗します。

#![allow(unused)]
#![feature(impl_trait_in_assoc_type)]
#![feature(associated_type_defaults)]

fn main() {
trait Foo {
    type RPITIT = impl Sized;

    fn bar() -> Self::RPITIT {
        01i32
    }
}
}

下流の impl が、理論上は bar の実装を提供せずに RPITIT の実装を提供できるため、失敗します。

error[E0308]: mismatched types
--> src/lib.rs:8:9
 |
5 |     type RPITIT = impl Sized;
 |     ------------------------- associated type defaults can't be assumed inside the trait defining them
6 |
7 |     fn bar() -> Self::RPITIT {
 |                 ------------ expected `<Self as Foo>::RPITIT` because of return type
8 |         01i32
 |         ^^^^^ expected associated type, found `i32`
 |
 = note: expected associated type `<Self as Foo>::RPITIT`
                       found type `i32`

整形式性検査

通常の関連型と同様に、RPITIT の整形式性を検査します。

複製された早期束縛ライフタイムを元のライフタイムに結び付けるライフタイム境界を predicates_of に追加し、RPITIT の由来となるメソッドの WF 型を継承する assumed_wf_types を実装したため(#113704)、通常の GAT であるかのように GAT の WF 検査を行っても問題はありません。

壊れているもの、奇妙なもの、その他

特殊化はかなり壊れています

上で説明した「デフォルト trait メソッド」は特殊化とうまく相互作用しません。なぜなら、これらの射影境界は trait のデフォルトメソッドにのみインストールし、impl メソッドにはインストールしないためです。特殊化はすでにかなり壊れているので詳細には踏み込みませんが、現在これは以下で追跡されているバグです。 * tests/ui/impl-trait/in-trait/specialization-broken.rs

射影には分散がありません

射影には分散がないため、このコードは失敗します。

#![allow(unused)]
#![feature(return_position_impl_trait_in_trait)]

fn main() {
trait Foo {
    // 以下の RPITIT は `'lt` をキャプチャしないことに注意してください。
    fn bar<'lt: 'lt>() -> impl Eq;
}

fn test<'a, 'b, T: Foo>() -> bool {
    <T as Foo>::bar::<'a>() == <T as Foo>::bar::<'b>()
    //~^ ERROR
    // (`'a == 'b` が必要)
}
}

これは、ライフタイムをキャプチャしていない場合でも、<T as Foo>::Rpitit<'a><T as Foo>::Rpitit<'b> を関連付けることができないためです。通常の opaque 型を使用していれば、これは機能します。なぜなら、そのライフタイムパラメーターに関して双変になるためです。

#![allow(unused)]
#![feature(return_position_impl_trait_in_trait)]

fn main() {
fn bar<'lt: 'lt>() -> impl Eq {
    ()
}

fn test<'a, 'b>() -> bool {
    bar::<'a>() == bar::<'b>()
}
}

ただし、RPITIT はいずれにせよ、スコープ内のすべてのライフタイムをキャプチャするようにキャプチャ動作が変更される可能性が高いため、おそらくこれは問題ありません。射影を関連付ける際に RPITIT の分散を考慮するようにすれば、後で前方互換な方法で緩和することもできます。


  1. これは compiler-errors の用語であり、正確だと主張しているわけではありません :^)