早期束縛パラメーターと後期束縛パラメーター
注記: この章では、主に早期束縛/後期束縛を、関数アイテム型/関数定義について論じる場合にのみ関係するものとして扱います。これは完全には正しくない可能性があり、async ブロックとクロージャについても、この章である程度論じるべきでしょう。
早期束縛パラメーターと後期束縛パラメーターの区別が導入された当時の、これらのブログ記事も参照してください: Intermingled parameter lists および Intermingled parameter lists, take 2。
「早期」束縛または「後期」束縛であるとはどういう意味か
すべての関数定義には、関数アイテム型として知られる Fn* トレイトを実装する、対応する ZST があります。
この章のこの部分では、早期束縛ジェネリックパラメーターと後期束縛ジェネリックパラメーターの違いを説明するうえで有用な文脈として、関数アイテム型の「脱糖」について少し説明します。
まず、ジェネリックパラメーターを含まない非常に単純な例から始めましょう。
#![allow(unused)]
fn main() {
fn foo(a: String) -> u8 {
1
/* 中略 */
}
}
foo に対応する関数アイテム型と、それに関連付けられた Fn impl の定義を明示的に書き出すと、次のようになります。
struct FooFnItem;
impl Fn<(String,)> for FooFnItem {
type Output = u8;
/* fn call(&self, ...) -> ... { ... } */
}
FnMut/FnOnce トレイトの組み込み impl、および Copy と Clone の impl は、簡潔にするために省かれています(ただし、これらのトレイトは関数アイテム型に対して実装されています)。
もう少し複雑な例では、関数にジェネリックパラメーターを導入します。
#![allow(unused)]
fn main() {
fn foo<T: Sized>(a: T) -> T {
a
/* 中略 */
}
}
定義を書き出すと、次のようになります。
struct FooFnItem<T: Sized>(PhantomData<fn(T) -> T>);
impl<T: Sized> Fn<(T,)> for FooFnItem<T> {
type Output = T;
/* fn call(&self, ...) -> ... { ... } */
}
関数アイテム型 FooFnItem は、関数 foo 上で定義された型パラメーター T に対してジェネリックであることに注意してください。
しかし、関数上で定義されたすべてのジェネリックパラメーターが、関数アイテム型上にも定義されるわけではありません。これは次の例で示されています。
#![allow(unused)]
fn main() {
fn foo<'a, T: Sized>(a: &'a T) -> &'a T {
a
/* 中略 */
}
}
その「脱糖」された形式は、次のようになります。
struct FooFnItem<T: Sized>(PhantomData<for<'a> fn(&'a T) -> &'a T>);
impl<'a, T: Sized> Fn<(&'a T,)> for FooFnItem<T> {
type Output = &'a T;
/* fn call(&self, ...) -> ... { ... } */
}
関数 foo のライフタイムパラメーター 'a は、関数アイテム型 FooFnItem には存在せず、代わりに引数型を表すためだけに、組み込み impl 上に導入されます。
ジェネリックパラメーターがすべて関数アイテム型上で定義されるわけではないということは、関数を呼び出す際にジェネリック引数が提供される段階が 2 つあることを意味します。
- 関数を名指しする段階(例:
let a = foo;)では、FooFnItemに対する引数が提供されます。 - 関数を呼び出す段階(例:
a(&10);)では、組み込み impl 上で定義された任意のパラメーターが提供されます。
この 2 段階の仕組みが、早期と後期という名前付けの由来です。早期束縛パラメーターは最も早い段階(関数を名指しする段階)で提供されるのに対し、後期束縛パラメーターは最も遅い段階(関数を呼び出す段階)で提供されます。
前の例の脱糖を見ると、T は早期束縛型パラメーターであり、'a は後期束縛ライフタイムパラメーターであることが分かります。なぜなら、T は関数アイテム型上に存在しますが、'a は存在しないからです。
各ジェネリックパラメーターに引数が提供される場所を注釈した、foo の呼び出し例を参照してください。
#![allow(unused)]
fn main() {
fn foo<'a, T: Sized>(a: &'a T) -> &'a T {
a
/* 中略 */
}
// ここでは、関数アイテム型上の型パラメーター `T` に
// 型引数 `String` を提供します
let my_func = foo::<String>;
// ここでは、組み込み impl 上のライフタイムパラメーター `'a` に
// (暗黙的に)ライフタイム引数が提供されます。
my_func(&String::new());
}
早期束縛パラメーターと後期束縛パラメーターの違い
高階ランク関数ポインターとトレイト境界
ジェネリックパラメーターが後期束縛であると、関数アイテムをより柔軟に使用できます。
たとえば、早期束縛ライフタイムパラメーターを持つ関数 foo と、後期束縛ライフタイムパラメーター 'a を持つ関数 bar がある場合、次のような組み込み Fn impl が得られます。
impl<'a> Fn<(&'a String,)> for FooFnItem<'a> { /* ... */ }
impl<'a> Fn<(&'a String,)> for BarFnItem { /* ... */ }
bar 関数は厳密により柔軟なシグネチャを持っています。なぜなら、その関数アイテム型は任意のライフタイムを持つ借用で呼び出せるのに対し、foo の関数アイテム型は、関数アイテム型上のライフタイムと同じライフタイムを持つ借用でしか呼び出せないからです。
これは、foo の関数アイテム型を異なるライフタイムで複数回呼び出そうとするだけで示せます。
#![allow(unused)]
fn main() {
// `'a: 'a` 境界により、このライフタイムは早期束縛になります。
fn foo<'a: 'a>(b: &'a String) -> &'a String { b }
fn bar<'a>(b: &'a String) -> &'a String { b }
// 早期束縛ジェネリックパラメーターは、関数 `foo` を
// 名指しするときに、ここでインスタンス化されます。`'a` は早期束縛であるため、引数が提供されます。
let f = foo::<'_>;
// 両方の関数引数は同じライフタイムを持つ必要があります。なぜなら、
// ライフタイムパラメーターが早期束縛であるということは、`f` が
// 1 つの特定のライフタイムに対してのみ呼び出し可能であることを意味するからです。
//
// これは異なるライフタイムの借用で呼び出しているため、借用チェッカーは
// ここでエラーを出します。
f(&String::new());
f(&String::new());
}
foo 上のライフタイムパラメーターが早期束縛であるため、f のすべての呼び出し元は同じライフタイムを持つ借用を提供する必要があります。
この例では、foo の関数アイテム型を 2 回呼び出しており、そのたびに一時値の借用を渡しています。
これら 2 つの借用は、ライフタイムが重なり合うことはあり得ません。一時値は関数呼び出しの間だけ生存し、その後は生存しないため、コンパイルエラーになります。
foo 上のライフタイムパラメーターが後期束縛であれば、各呼び出し元が自分の借用に対して異なるライフタイム引数を提供できるため、これはコンパイル可能になります。
上で定義した bar 関数を使ってこれを示す、次の例を参照してください。
#![allow(unused)]
fn main() {
fn foo<'a: 'a>(b: &'a String) -> &'a String { b }
fn bar<'a>(b: &'a String) -> &'a String { b }
// 早期束縛パラメーターはここでインスタンス化されますが、`'a` は
// 後期束縛であるため、ここでは提供されません。
let b = bar;
// 後期束縛パラメーターは各呼び出し箇所で個別にインスタンス化されるため、
// 各呼び出し元が異なるライフタイムを使用できます。
b(&String::new());
b(&String::new());
}
これは、関数アイテム型を高ランク関数ポインタへ型強制し、高ランクの Fn トレイト境界を証明できる能力に反映されています。
これは次の例で示せます。
#![allow(unused)]
fn main() {
// `'a: 'a` 境界により、このライフタイムは早期束縛になります。
fn foo<'a: 'a>(b: &'a String) -> &'a String { b }
fn bar<'a>(b: &'a String) -> &'a String { b }
fn accepts_hr_fn(_: impl for<'a> Fn(&'a String) -> &'a String) {}
fn higher_ranked_trait_bound() {
let bar_fn_item = bar;
accepts_hr_fn(bar_fn_item);
let foo_fn_item = foo::<'_>;
// エラー
accepts_hr_fn(foo_fn_item);
}
fn higher_ranked_fn_ptr() {
let bar_fn_item = bar;
let fn_ptr: for<'a> fn(&'a String) -> &'a String = bar_fn_item;
let foo_fn_item = foo::<'_>;
// エラー
let fn_ptr: for<'a> fn(&'a String) -> &'a String = foo_fn_item;
}
}
これらのどちらの場合も、借用チェッカーは foo_fn_item が任意のライフタイムの借用で呼び出し可能だとはみなさないため、エラーになります。
これは、foo のライフタイムパラメータが早期束縛であり、その結果 foo_fn_item が FooFnItem<'_> という型を持つためです。この型は、脱糖された Fn impl が示すように、同じライフタイム '_ の借用でのみ呼び出し可能です。
遅延束縛パラメータが存在する場合の Turbofish
前述のように、早期束縛パラメータと遅延束縛パラメータの区別は、ジェネリックパラメータがインスタンス化される場所が 2 つあることを意味します。
- 関数に名前を付けるとき(早期)
- 関数を呼び出すとき(遅延)
現在、呼び出しステップ中に遅延束縛パラメータのジェネリック引数を明示的に指定する構文はありません。ジェネリック引数を指定できるのは、関数に名前を付けるときの早期束縛パラメータに対してのみです。
構文 foo::<'static>(); は、関数呼び出しの一部であるにもかかわらず、(foo::<'static>)(); として振る舞い、関数アイテム型上の早期束縛ジェネリックパラメータをインスタンス化します。
次の例を見てください。
#![allow(unused)]
fn main() {
fn foo<'a>(b: &'a u32) -> &'a u32 { b }
let f /* : FooFnItem<????> */ = foo::<'static>;
}
上の例はエラーになります。ライフタイムパラメータ 'a は遅延束縛であり、「関数に名前を付ける」ステップの一部としてインスタンス化できないためです。
ライフタイムパラメータを早期束縛にすると、このコードはコンパイルされるようになります。
#![allow(unused)]
fn main() {
fn foo<'a: 'a>(b: &'a u32) -> &'a u32 { b }
let f /* : FooFnItem<'static> */ = foo::<'static>;
}
現在のコンパイラ実装が目指しているのは、早期束縛ライフタイムパラメータと遅延束縛ライフタイムパラメータの両方を持つ関数に対してライフタイム引数を指定した場合にエラーにすることです。 実際には、過度な破壊的変更を避けるため、一部のケースは実際には将来互換性警告にとどまっています(#42868)。
- ライフタイム引数の数が早期束縛ライフタイムパラメータの数と同じ場合、エラーではなく FCW が出力されます
- メソッド呼び出し構文を使用している場合、エラーは常に FCW に格下げされます
これを示すために、さまざまな種類の関数を書き出し、それぞれに遅延束縛ライフタイムと早期束縛ライフタイムの両方を与えることができます。
fn free_function<'a: 'a, 'b>(_: &'a (), _: &'b ()) {}
struct Foo;
trait Trait: Sized {
fn trait_method<'a: 'a, 'b>(self, _: &'a (), _: &'b ());
fn trait_function<'a: 'a, 'b>(_: &'a (), _: &'b ());
}
impl Trait for Foo {
fn trait_method<'a: 'a, 'b>(self, _: &'a (), _: &'b ()) {}
fn trait_function<'a: 'a, 'b>(_: &'a (), _: &'b ()) {}
}
impl Foo {
fn inherent_method<'a: 'a, 'b>(self, _: &'a (), _: &'b ()) {}
fn inherent_function<'a: 'a, 'b>(_: &'a (), _: &'b ()) {}
}
次に、最初のケースとして、各関数を単一のライフタイム引数(1 つの早期束縛ライフタイムパラメータに対応)で呼び出し、ハードエラーではなく FCW だけになることを確認できます。
#![allow(unused)]
#![deny(late_bound_lifetime_arguments)]
fn main() {
fn free_function<'a: 'a, 'b>(_: &'a (), _: &'b ()) {}
struct Foo;
trait Trait: Sized {
fn trait_method<'a: 'a, 'b>(self, _: &'a (), _: &'b ());
fn trait_function<'a: 'a, 'b>(_: &'a (), _: &'b ());
}
impl Trait for Foo {
fn trait_method<'a: 'a, 'b>(self, _: &'a (), _: &'b ()) {}
fn trait_function<'a: 'a, 'b>(_: &'a (), _: &'b ()) {}
}
impl Foo {
fn inherent_method<'a: 'a, 'b>(self, _: &'a (), _: &'b ()) {}
fn inherent_function<'a: 'a, 'b>(_: &'a (), _: &'b ()) {}
}
// 早期束縛パラメータと同じ数の引数を指定することは、
// 常に将来互換性警告になります
Foo.trait_method::<'static>(&(), &());
Foo::trait_method::<'static>(Foo, &(), &());
Foo::trait_function::<'static>(&(), &());
Foo.inherent_method::<'static>(&(), &());
Foo::inherent_function::<'static>(&(), &());
free_function::<'static>(&(), &());
}
2 番目のケースでは、ライフタイムパラメータの数(早期束縛か遅延束縛かを問わず)より多いライフタイム引数で各関数を呼び出し、メソッド呼び出しは FCW になる一方で、自由関数や関連関数はハードエラーになることを確認します。
#![allow(unused)]
fn main() {
fn free_function<'a: 'a, 'b>(_: &'a (), _: &'b ()) {}
struct Foo;
trait Trait: Sized {
fn trait_method<'a: 'a, 'b>(self, _: &'a (), _: &'b ());
fn trait_function<'a: 'a, 'b>(_: &'a (), _: &'b ());
}
impl Trait for Foo {
fn trait_method<'a: 'a, 'b>(self, _: &'a (), _: &'b ()) {}
fn trait_function<'a: 'a, 'b>(_: &'a (), _: &'b ()) {}
}
impl Foo {
fn inherent_method<'a: 'a, 'b>(self, _: &'a (), _: &'b ()) {}
fn inherent_function<'a: 'a, 'b>(_: &'a (), _: &'b ()) {}
}
// 早期束縛パラメータより多くの引数を指定することは、
// メソッド呼び出し構文を使用している場合には
// 将来互換性警告になります。
Foo.trait_method::<'static, 'static, 'static>(&(), &());
Foo.inherent_method::<'static, 'static, 'static>(&(), &());
// しかし、メソッド呼び出し構文を使用していない場合はハードエラーになります。
Foo::trait_method::<'static, 'static, 'static>(Foo, &(), &());
Foo::trait_function::<'static, 'static, 'static>(&(), &());
Foo::inherent_function::<'static, 'static, 'static>(&(), &());
free_function::<'static, 'static, 'static>(&(), &());
}
遅延束縛ライフタイムパラメータと早期束縛ライフタイムパラメータの両方に十分な数のライフタイム引数を指定した場合でも、これらの引数は、遅延束縛パラメータに提供されるライフタイムを注釈するために実際に使用されるわけではありません。
これは、非 static な借用を提供しながら、関数に 'static を turbofish することで示せます。
#![allow(unused)]
fn main() {
struct Foo;
impl Foo {
fn inherent_method<'a: 'a, 'b>(self, _: &'a (), _: &'b String ) {}
}
Foo.inherent_method::<'static, 'static>(&(), &String::new());
}
これは、&String::new() 関数引数が 'static ライフタイムを持たないにもかかわらずコンパイルされます。これは、関数を実際に呼び出すときに、「余分な」ライフタイム引数が遅延束縛パラメータについて考慮されるのではなく、破棄されるためです。
遅延束縛パラメータを持つ型のライブネス
関数アイテム型を含む型の outlives 境界をチェックするとき、早期束縛パラメータを考慮します。 例:
#![allow(unused)]
fn main() {
fn foo<T>(_: T) {}
fn requires_static<T: 'static>(_: T) {}
fn bar<T>() {
let f /* : FooFnItem<T> */ = foo::<T>;
requires_static(f);
}
}
型パラメーター T は早期束縛されるため、foo の関数アイテム型を脱糖すると、おおよそ struct FooFnItem<T> のようになります。
そのため、FooFnItem<T>: 'static が成り立つためには、T: 'static も成り立つことを要求しなければなりません。そうしないと、健全性バグにつながることになります。
残念ながら、コンパイラーのバグにより、早期束縛されたライフタイムを考慮していません。これが未解決の健全性バグ #84366 の原因です。 これは、生存性/型の outlives 境界について、早期束縛パラメーターと遅延束縛パラメーターの「違い」を示すことが不可能であることを意味します。遅延束縛できる唯一のジェネリックパラメーターの種類はライフタイムですが、それが正しく処理されていないためです。
それでも理論上は、#84366 が修正されれば、以下のコード例はそのような違いを示すはずです。
#![allow(unused)]
fn main() {
fn early_bound<'a: 'a>(_: &'a String) {}
fn late_bound<'a>(_: &'a String) {}
fn requires_static<T: 'static>(_: T) {}
fn bar<'b>() {
let e = early_bound::<'b>;
// これはエラーになる*べき*だが、ならない
requires_static(e);
let l = late_bound;
// これは正しくエラーにならない
requires_static(l);
}
}
パラメーターが遅延束縛されるための要件
ライフタイムパラメーターでなければならない
型パラメーターと Const パラメーターは遅延束縛できません。これは、dyn for<T> Fn(Box<T>) や for<T> fn(Box<T>) のような型をサポートする方法がないためです。
そのような型を呼び出すには、基礎となる関数を単相化できる必要がありますが、動的ディスパッチによる間接参照を介してそれを行うことはできません。
where 句で使用されてはならない
現在、ジェネリックパラメーターが where 句で使用されている場合、そのパラメーターは早期束縛されなければなりません。 例:
#![allow(unused)]
fn main() {
trait Trait<'a> {}
fn foo<'a, T: Trait<'a>>(_: &'a String, _: T) {}
}
この例では、ライフタイムパラメーター 'a は where 句 T: Trait<'a> に現れるため、早期束縛されるものと見なされます。
これは、'a: 'a のような「自明な」where 句や、関数引数の wellformedness によって暗黙的に導かれる where 句についても当てはまります。例:
#![allow(unused)]
fn main() {
fn foo<'a: 'a>(_: &'a String) {}
fn bar<'a, T: 'a>(_: &'a T) {}
}
これら両方の関数において、ライフタイムパラメーター 'a は早期束縛されるものと見なされます。たとえ、それらが使用されている where 句が、実際には呼び出し側に何ら制約を課していないとも言える場合であってもです。
この制限の理由は、次の 2 つの組み合わせです。
- 遅延束縛パラメーター上の境界は、それらがインスタンス化されるまで証明できません
- 関数ポインターとトレイトオブジェクトには、基礎となる関数からの、まだ証明されていない where 句を表現する方法がありません
次の例を考えてみます。
#![allow(unused)]
fn main() {
trait Trait<'a> {}
fn foo<'a, T: Trait<'a>>(_: &'a T) {}
let f = foo::<String>;
let f = f as for<'a> fn(&'a String);
f(&String::new());
}
型チェック中のどこかの時点で、このコードに対してエラーが出力されるべきです。String はどのライフタイムについても Trait を実装していないためです。
ライフタイム 'a が遅延束縛されている場合、これをチェックするのは難しくなります。
foo を名指しする時点では、T: Trait<'a> トレイト境界の一部としてどのライフタイムを使用すべきかがわかりません。まだインスタンス化されていないためです。
関数アイテム型を関数ポインターに型強制するときには、関数を呼び出す際に証明されなければならない String: Trait<'a> トレイト境界を追跡する方法がありません。
ライフタイム 'a が早期束縛されている場合(rustc の現在の実装ではそうなっています)、関数 foo を名指しする時点でトレイト境界をチェックできます。
where 句で使用されるパラメーターを早期束縛にすることにより、関数に定義された where 句をチェックする自然な場所が得られます。
最後に、ライフタイムが暗黙の境界で使用されている場合には、早期束縛されることを要求しません。例:
#![allow(unused)]
fn main() {
fn foo<'a, T>(_: &'a T) {}
let f = foo;
f(&String::new());
f(&String::new());
}
このコードはコンパイルできるため、ライフタイムパラメーターが遅延束縛されていることが示されます。たとえ 'a が型 &'a T の中で使用されており、それによって T: 'a が成り立つことが暗黙的に要求されるとしてもです。
暗黙の境界は特別に扱うことができます。暗黙の境界を導入する型はいずれも関数ポインター型のシグネチャ内に存在するため、関数を呼び出すときに T: 'a を証明すべきであることがわかるからです。
引数型によって制約されていなければならない
関数アイテム型上の組み込み impl が、制約されていないジェネリックパラメーターを持つことにならないようにすることが重要です。これは非健全性につながる可能性があるためです。 これはユーザーが書いた impl に適用される制限と同じ種類のものです。たとえば、次のコードはエラーになります。
#![allow(unused)]
fn main() {
trait Trait {
type Assoc;
}
impl<'a> Trait for u8 {
type Assoc = &'a String;
}
}
関数アイテム上の組み込み impl に対応する例は、次のようになります。
fn foo<'a>() -> &'a String { /* ... */ }
ライフタイムパラメーター 'a が遅延束縛される場合、制約されていないライフタイムを持つ組み込み impl になることになります。'a が遅延束縛されているものとして、関数アイテム型とその impl の脱糖を手で書き下すことで、これを示すことができます。
// 注: これは単なるデモンストレーションであり、実際には `'a` は早期束縛される
struct FooFnItem;
impl<'a> Fn<()> for FooFnItem {
type Output = &'a String;
/* fn call(...) -> ... { ... } */
}
このような状況を避けるため、'a は早期束縛されるものと見なします。これにより、impl 上のライフタイムは self 型によって制約されます。
struct FooFnItem<'a>(PhantomData<fn() -> &'a String>);
impl<'a> Fn<()> for FooFnItem<'a> {
type Output = &'a String;
/* fn call(...) -> ... { ... } */
}