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

新しいソルバーにおける opaque 型

新しいソルバーでの opaque 型 の扱い方は、古い実装とは異なります。 これは、新しいソルバーにおける挙動についての自己完結した説明であるべきです。

非定義使用 vs 定義使用

非定義使用と定義使用の区別は、推論の挙動を決定し、抽象化の厳密さを制御します。定義使用は背後にある hidden type を明らかにしますが、非定義使用は剛性を強制し、その型を抽象的なプレースホルダーとして扱います。

非定義使用

この概念により、opaque 型は剛性を持つようになります。背後にある型を未知として扱うことで、ソルバーは抽象化を維持します。

#![allow(unused)]
fn main() {
fn foo() -> impl Copy {
    let x: u32 = 56;
    x 
}

fn bar() {
    let x = foo();
    // 'foo' が u32 を返すことは分かっていても、ソルバーはここで
    // 剛性を強制し、これは非定義使用です。
}
}

定義使用

ここで opaque 型が実際に定義されます。定義使用は、抽象化の背後にある具象型が正確に何であるかをコンパイラに伝えます。

#![allow(unused)]
fn main() {
fn foo() -> impl Copy {
    let x: u32 = 56;
    // 戻り値 x は hidden type を u32 として定義し、これは定義使用です。
    x
}
}

opaque は alias 型である

opaque 型は、可能な限り、他の alias、特に associated type と同じように扱われます。 挙動の相違はできるだけ少なくあるべきです。

これは望ましいことです。なぜなら、opaque 型は他の alias 型と非常によく似ており、hidden type へ正規化でき、完全性に関しても同じ要件を持つためです。 このように扱うことで、コードを共有して型システムの複雑さも低減できます。 opaque 型を個別に扱わなければならない場合、より複雑なルールや新しい種類の相互作用が生じます。 implicit-negative モードでは opaque 型を他の alias と同様に扱う必要があるため、モード間に大きな差異があることも複雑さを増します。

未解決の疑問: ここに代替アプローチはあるか。たとえば、opaque 型をより剛性のある型のように扱い、インスタンス化できる場所をより限定する方法はあるか。coherence の間は、それでも通常の alias である必要がある

opaque に対する normalizes-to

source

normalizes-to は、新しいソルバーにおいて alias の 1 ステップ正規化の挙動を定義するために使用されます。<<T as IdInner>::Assoc as IdOuter>::Assoc はまず <T as IdInner>::Assoc に正規化され、その後 T に正規化されます。これは、正規化される AliasTy と期待される Term の両方を受け取ります。実際の正規化に normalizes-to を使用するには、期待される項を単に未制約の推論変数にできます。

定義スコープ内および implicit-negative coherence モード内の opaque 型については、これは常に 2 ステップで行われます。定義スコープ外では、opaque に対する normalizes-to は常に Err(NoSolution) を返します。

まず、期待される型を hidden type として割り当てようとします。

implicit-negative coherence モードでは、これは現在、opaque 型ストレージと相互作用することなく、常に曖昧性になります。代わりに、すべての opaque 型を「定義」できるようにし、最後にそれらの推論された型を破棄することもできます。この場合、coherence 中に opaque 型が複数回使用されたときの挙動が変わります: example

定義スコープ内では、まず opaque の型引数と const 引数がすべてプレースホルダーであるかどうかを確認します: source。このチェックが曖昧であれば曖昧性を返し、失敗した場合は Err(NoSolution) を返します。このチェックは、borrowck の最後でのみ確認される領域を無視します。成功した場合は続行します。

次に、opaque のジェネリック引数を、opaque 型ストレージにすでに存在する任意の opaque 型の引数と 意味論的に 単一化できるかどうかを確認します。できる場合は、以前に格納された型を、この normalizes-to 呼び出しの期待される型と単一化します: source1

できない場合は、期待される型を opaque 型ストレージに挿入します: source2。 最後に、opaque の item bounds が期待される型に対して成り立つかどうかを確認します: source

正規化可能な alias の alias-bounds を使用する

https://github.com/rust-lang/trait-system-refactor-initiative/issues/77

正規化可能な alias に対して AliasBound 候補を使用することは、一般にはできません。なぜなら、associated type は ParamEnv 候補を介して正規化した結果の型よりも強い境界を持つことがあるためです。

これらの候補は、正確な正規化戦略をユーザーから見えるものにしてしまいます。そうでなければ、正規化を積極的に行うかどうかは、ほとんど観測できません。どこで正規化するかは、古いソルバーのサポートを削除した後に変更したい可能性が高いため、それは望ましくありません。

opaque 型はどこでも定義できる

opaque 型は、その定義スコープ内であれば、単に型を関連付ける場合でも trait ソルバー内でも、どこでも定義できます。これにより、順序依存性と不完全性が取り除かれます。これがない場合、ゴールの結果は微妙な理由によって異なることがあります。たとえば、opaque の最初の定義使用より前に、その opaque を使ってゴールを評価しようとするかどうかなどです。

定義スコープ内の higher ranked opaque 型

これらはサポートされておらず、現時点で定義しようとすると常にエラーになるべきです。

FIXME: opaque 型ストレージで opaque 型を検索すると、今では領域を単一化できるため、opaque 型がプレースホルダーを参照していないことを積極的に確認する必要があります。そうしないと、プレースホルダーが漏出してしまいます。

member constraints

member constraints の扱いは新しいソルバーでも変わりません。それについては 関連する既存の章 を参照してください。

opaque 型に対するメソッド呼び出し

FIXME: 定義スコープ内で、まだ未制約の opaque 型に対してメソッドを呼び出すことを引き続きサポートする必要があります。これをどのように行うのが最善かは不明です。

#![allow(unused)]
fn main() {
use std::future::Future;
use futures::FutureExt;

fn go(i: usize) -> impl Future<Output = ()> + Send + 'static {
    async move {
        if i != 0 {
            // これは定義スコープ内で `impl Future<Output = ()>` を返します。
            // この時点では、その opaque の具象型は分かっていません。
            // 現在は opaque を既知の型として扱い成功しますが、
            // 「健全に実装するのが最も容易」という観点からは、
            // これが曖昧であるとよいでしょう。
            go(i - 1).boxed().await;
        }
    }
}
}

  1. FIXME: args がプレースホルダーであることを要求し、領域は常に推論変数であることを考えると、理想的にはこれは一意の候補のみをもたらすべきです

  2. FIXME: なぜ期待される型が剛性を持つかどうかを確認するのでしょうか。