型強制
型強制は、値を別の型へ変換する暗黙的な操作です。型強制サイトとは、型強制を暗黙的に実行できる位置のことです。型強制サイトには次の 2 種類があります。
- 1対1
- LUB(Least-Upper-Bound、最小上界)
#![allow(unused)]
fn main() {
let one_to_one_coercion: &u32 = &mut 8;
let lub_coercion = match my_bool {
true => &mut 10,
false => &12,
};
}
どのような型強制が存在するか、およびどの式が型強制サイトであるかについては、型強制に関する Reference ページを参照してください: https://doc.rust-lang.org/reference/type-coercions.html
1対1の型強制
1対1の型強制では、単一の型から既知のターゲット型へ型強制します。上の例では、これは &mut u32 から &u32 への型強制です。
1対1の型強制は、FnCtxt::coerce を呼び出すことで実行できます。
LUB 型強制
LUB 型強制では、複数のソース型を何らかの未知のターゲット型へ型強制します。1対1の型強制とは異なり、LUB 型強制は、すべてのソース型が型強制されるターゲット型を生成します。
上の例では、これは &mut i32 と &i32 の両方に対する LUB 型強制であり、ターゲット型 &i32 を生成します。
「LUB 型強制」(Least-Upper-Bound 型強制)という名前は、この型強制が型の集合を受け取り、両方のソース型を型強制/サブタイプ化できる、型強制/サブタイプ化された最小の型を計算する方法に由来します。
LUB 型強制を実行する一般的な処理は次のとおりです。
// * 1
let mut coerce = CoerceMany::new(intial_lub_ty);
for expr in exprs {
// * 2
let expr_ty = fcx.check_expr_with_expectation(expr, expectation);
coerce.coerce(fcx, &cause, expr, expr_ty);
}
// * 3
let final_ty = coerce.complete(fcx);
ここにはいくつかの重要なステップがあります。
CoerceMany値を作成し、初期 lub を選ぶ- 各式を型検査し、その型を LUB 型強制の一部として登録する
- LUB 型強制を完了させ、結果として得られる LUB 化された型を取得する
ステップ 1
まず CoerceMany 値を作成します。これは LUB 型強制に必要なすべての状態を格納します。1対1の型強制とは異なり、LUB 型強制は単一の関数呼び出しではありません。これは、型検査と LUB 型強制の進行を織り交ぜたいからです。
CoerceMany の作成には、何らかの initial_lub 型を渡します。これは型強制のターゲットとは異なります。ターゲットは、(1対1の型強制とは異なり)入力ではなく、LUB 型強制の出力です。
初期 lub ty は、この LUB 型強制の対象となる式の Expectation から導出されるべきです。これにより、LUB 型強制の計算から生じる推論制約を、その LUB 型強制に参加する後続の式を型検査するために使われる Expectation へ伝播できます。
これが及ぼす効果についての詳細は、“unnecessary inference constraints” 見出しを参照してください。
使用する Expectation がない場合は、初期 lub ty 用に新しい推論変数を作成するべきです。
ステップ 2
次に、LUB 型強制に参加する各式について、それを型検査してから、その型を指定して CoerceMany::coerce を呼び出します。
場合によっては、LUB 型強制に参加する式が HIR に実際には存在しないことがあります。たとえば、オペランドを持たない break 式や return 式を処理するときには、() が LUB 型強制に参加する必要があります。
このような場合には、CoerceMany::coerce_forced_unit メソッドを使用できます。
CoerceMany::coerce メソッドと coerce_forced_unit メソッドはいずれも、新しい型によって LUB 型強制が充足不能になる場合にエラーを出力します。この場合、LUB 型強制の最終的な型はエラー型になります。
ステップ 3
最後に、すべての式が型強制されたら、CoerceMany::complete を呼び出すことで LUB 型強制の最終的な型を取得できます。
LUB 型強制の結果として得られる型は、CoerceMany の構築時に渡された初期 lub 型とは意味のある形で異なります。常に LUB 型強制の結果として得られる型を受け取り、それに対して必要なチェックを実行するべきです。
実装上のニュアンス
調整
型強制操作が成功したときには、それがどの種類の型強制であったかを記録します。たとえば、unsize 型強制や autoderef などです。これは型強制操作の一部として処理され、進行中の TypeckResults に調整のリストを書き込みます。
THIR を構築するときには、TypeckResults に格納されている調整を取り出し、すべての型強制ステップを明示的にします。コンパイラのこの時点以降では、型強制という概念は実質的には存在せず、MIR における明示的なキャストとサブタイプ化だけがあります。
TODO: ここに調整の章を書いてリンクする
CoerceMany はどのように動作するか
CoerceMany は、現在の lub ty と何らかの新しいソース型を繰り返し受け取り、両方の型を型強制できる新しい lub ty を計算することで動作します。型のペアを受け取り、何らかの新しい第 3 の型を計算する中核ロジックは、try_find_coercion_lub にあります。
#![allow(unused)]
fn main() {
fn foo() {}
fn bar() {}
let a = match my_bool {
true => foo,
true if other_bool => foo,
false => bar,
}
}
この例では、match 式を型検査するときに LUB 型強制が実行されます。この LUB 型強制は、let 文に既知の型がないため、何らかの推論変数 ?x を初期 lub ty として開始します。
この LUB 型強制に参加する式は 3 つあります。LUB 型強制の最初の式は特別で、既存の初期 lub ty とともに新しい型を計算するのではなく、最初の式から初期 lub ty へ直接型強制します。
true => foo,を型検査した後、最終的に型FnDef(Foo)が得られます。次にCoerceMany::coerceを呼び出します。これはFnDef(Foo)から?xへの1対1の型強制を実行します。これにより?x=FnDef(Foo)が推論され、LUB 型強制の新しい lub ty が得られます。true if other_bool => foo,を型検査した後、再び最終的に型FnDef(Foo)が得られます。次にCoerceMany::coerceを呼び出します。これは、以前の lub ty(FnDef(Foo))とこの式の型(FnDef(Foo))から新しい lub ty を計算しようとします。これにより lub tyFnDef(Foo)が得られます。false => bar,を型検査した後、最終的に型FnDef(Bar)が得られます。次にCoerceMany::coerceを呼び出します。これは、以前の lub ty(FnDef(Foo))とこの式の型(FnDef(Bar))から新しい lub ty を計算しようとします。この場合、両方の関数アイテム型を関数ポインタへ型強制することを選ぶため、型fn() -> ()が得られます。
これにより、LUB 型強制の最終的な型として fn() -> () が得られます。
推移的な型強制
CoerceMany の、現在のターゲット型を新しい型へ型強制しようと繰り返し試みるアルゴリズムは、現在のところ「推移的な型強制」をもたらします。LUB 型強制のあるステップで式を型強制し、その後のステップでその式をさらに型強制することが可能です。
```rust
struct Foo;
use std::ops::Deref;
impl Deref for Foo {
type Target = [u8; 2];
fn deref(&self) -> &[u8; 2] {
&[1; _]
}
}
fn main() {
match () {
_ if true => &Foo,
_ if true => &[1_u8; 2],
_ => &[1_u8; 2] as &[u8],
};
}
ここでは、初期 lub 型が ?x である LUB 型強制があります。最初のステップでは、&Foo から ?x への 1 対 1 の型強制を行います(最初のステップは特別であることを思い出してください)。
2 番目のステップでは、現在の lub 型である &Foo と、新しい型である &[u8; 2] から、新しい lub 型を計算します。この新しい lub 型は、最初の式に対して &Foo から &[u8; 2] への deref 型強制を行うことで、&[u8; 2] になります。
3 番目のステップでは、現在の lub 型である &[u8; 2] と、新しい型である &[u8] から、新しい lub 型を計算します。この新しい lub 型は、最初の 2 つの式に対して &[u8; 2] から &[u8] への unsizing 型強制を行うことで、&[u8] になります。
最初の式が 2 回型強制されている点に注意してください。1 回目は &Foo から &[u8; 2] への deref 型強制で、次に &[u8; 2] から &[u8] への unsizing 型強制です。
推移的な型強制の現在の実装は壊れており、前の例は実際には stable で ICE します。LUB 型強制を実行するロジックは推移的な型強制を問題なく生成できますが、コンパイラの残りの部分はそれらを処理できるようになっていません。
1 対 1 の型強制は、LUB 型強制が生成できる多くの種類の推移的な型強制も生成できません。たとえば、前の例を 1 対 1 の型強制に変えると、コンパイルエラーになります。
struct Foo;
use std::ops::Deref;
impl Deref for Foo {
type Target = [u8; 2];
fn deref(&self) -> &[u8; 2] {
&[1; _]
}
}
fn main() {
let a: &[u8] = &Foo;
}
ここでは &Foo から &[u8] への 1 対 1 の型強制を実行しようとしていますが、これは失敗します。deref 型強制か unsizing 型強制のどちらかしか実行できず、その 2 つを合成することはできないためです。
try_find_coercion_lub はどのように動作するか
LUB 型強制の新しい lub 型を計算する方法は 3 つあります。
- 現在の lub 型と新しい型の両方を関数ポインターに型強制する
- 現在の lub 型を新しい型に型強制する(またはその逆)
- 現在の lub 型と新しい型の相互上位型を計算する
残念ながら、実際の実装はこの点をかなり分かりにくくしています。
相互上位型の計算は、型強制が失敗した場合にすでに部分型付けを処理する 1 対 1 の型強制のロジックを再利用しているため、暗黙的に行われます。
さらに、現在の lub 型と新しい型の両方を関数ポインターに型強制しようとする際には、不要な型強制を避けるため、相互上位型の計算を積極的に試みます。
この関数の構造を改善して、概念モデルにより近づける余地はおそらくあります。
1 対 1 の型強制における use_lub フィールド
1 対 1 の型強制の実装は、LUB 型強制の一部として再利用されています。
シグネチャを関連付ける場合や、型強制が不可能な場合に部分型付けへフォールバックする場合に、LUB 型強制が一方向の部分型付けを使うのは誤りです。代わりに、2 つの型の相互上位型を計算したいのです。
Coerce の use_lub フィールドは、通常の部分型付けを行うか(1 対 1 の型強制の場合)、相互上位型を計算するか(LUB 型強制の場合)を切り替えるために存在します。
Lubbing
理論上、相互上位型の計算は、新しい推論変数 ?mutual_sup を作成し、lub_ty <: ?mutual_sup と new_ty <: ?mutual_sup を要求するだけの単純なものであるはずです。実際には、LUB 型強制は特別な TypeRelation である LatticeOp を使用します。
これは主に、高ランク型に対する部分型付け/汎化がかなり壊れていることを回避するためです。通常の部分型付けとは異なり、高ランク型に遭遇すると、lub 型関係は不変性に切り替わります。
これにより、高ランク型のバインダーが等価であることが強制され、「最も一般的な」バインダーを選ぶ必要がなくなります。そのようなバインダーを選ぶのは非常に困難です。
また、相互上位型を計算するプロセスが順序依存になることも避けられます。型 a と b が与えられたとき、a と b の相互上位型を計算した結果が、b と a の相互上位型を計算した結果と同じになると望ましいかもしれません。
高ランク型と部分型付けに関する現在の問題により、相互上位型を計算する素朴な方法を使った場合、この性質は成り立たなくなります。
型強制は MIR 構築中に明示的な MIR 操作へ変換されるため、LUB 型強制の最終的な型を計算するプロセスは HIR typeck 中にのみ発生します。これはまた、相互上位型を計算する挙動が型推論にのみ関係し、健全性には関係しないことを意味します。
注意事項
Probe
probe の内部から型強制する場合は注意が必要です。1 対 1 の型強制と LUB 型強制のどちらにも、probe によってロールバックできない副作用があるためです。
LUB 型強制は、型強制ステップが失敗するとエラーを出力します。これにより、probe の内部での使用に完全に適したものになります。
1-to-1 型強制と LUB 型強制はどちらも、成功時に型強制された式へ調整を適用します。つまり、probe の内部で型強制の試行が成功した場合、その probe は何もロールバックしてはなりません。
したがって、FnCtxt::coerce 呼び出しを commit_if_ok の内部にラップするのは正しいですが、coerce 呼び出しの後に Err を返す場合にそうするのは誤りです。また、probe の内部から FnCtxt::coerce を呼び出すのも誤りです。
CoerceMany は、probe または commit_if_ok の内部から決して使用すべきではありません。
Never-to-Any 型強制
never 型(!)から推論変数へ型強制すると、ターゲット型がその推論変数である NeverToAny 型強制になります。これは、その推論変数を never 型と単一化することとは微妙に異なります。
ある推論変数 ?x を ! と単一化するには、?x が実際に ! と等しい必要があります。しかし、NeverToAny 型強制では、?x は任意の可能な型に推論されることが許されます。
この違いは、型強制の初期 lub 型が推論変数である場合(たとえば、初期 lub 型に使用する Expectation がない場合)でも、部分型付けではなく型強制を使用することが重要であることを意味します。
never 型に推論してしまうのではなく、型強制を経由すべきだった箇所で誤って never 型に推論していたバグを修正する PR #147834 を参照してください。
部分型付けへのフォールバック
部分型付けは型強制ではありませんが、FnCtxt::coerce と CoerceMany::coerce/coerce_forced_unit はどちらも、部分型付けによって成功することがあります。
1 対 1 の型強制では、ソース型がターゲット型の部分型であることを強制しようとします。LUB 型強制では、既存のすべての型の上位型である型を計算しようとします。
たとえば、?x から u32 への 1 対 1 の型強制を実行すると、部分型付けにフォールバックし、?x eq u32 と推論します。これは、型強制が失敗した場合、その後に部分型付けを試みる必要がないことを意味します。
不要な推論制約
Expectation の型を初期 lub ty として使用すると、LUB 型強制に参加する式の型によって推論変数が制約される可能性があります。これらの推論変数は、実際には LUB 型強制の最終的な型によってのみ制約されればよいため、これは常に望ましいとは限りません。
#![allow(unused)]
fn main() {
fn foo<T>(_: T) {}
fn a() {}
fn b() {}
foo::<?x>(match my_bool {
true => a,
false => b,
})
}
ここでは、最初の式の型が FnDef(a) で、2 つ目の式の型が FnDef(b) である LUB 型強制があります。LUB 型強制の初期 lub ty として ?x を使用すると、次のような挙動になります。
- 式 1:
?x=FnDef(a)と推論する - 式 2:
FnDef(a), FnDef(b)の間の型強制 lub を見つけ、その結果fn() -> ()になる - LUB 型強制の最終的な型は
fn() -> ()です。?x eq fn() -> ()を等置しますが、?xは実際にはすでにFnDef(a)と推論されているため、これは実際にはFnDef(a) eq fn() -> ()を等置していることになり、これは成立しません
これらの望ましくない推論制約の一部(ただしすべてではありません)を避けるため、LUB 型強制の Expectation が推論変数である場合、それを初期 lub ty として使用しません。代わりに新しい推論変数を作成します。たとえば、上記のコードスニペットでは、実際には ?x を使用する代わりに、初期 lub ty 用に何らかの新しい推論変数 ?y を作成します。
- 式 1:
?y=FnDef(a)と推論する - 式 2:
FnDef(a), FnDef(b)の間の型強制 lub を見つけ、その結果fn() -> ()になる - LUB 型強制の最終的な型は
fn() -> ()であり、?x=fn() -> ()と推論する
新しい推論変数を作成しなかったことにより望ましくない推論制約が生じた事例については、#140283 を参照してください。
これは すべて のケースで不要な制約を避けるわけではなく、Expectation が推論変数であるという最も一般的なケースのみを避けます。理論上は、すべてのケースでこれらの制約を避けることが望ましいですが、それを行うにはかなり複雑な作業が必要になります。