Rust 1.82 で安定化し 1.87 で trait
にも拡張された impl use<.../>
という構文 (Precise Capturing, 拙訳: 精密キャプチャ) を, 備忘録として解説しておく.
抽象戻り型についておさらい
抽象戻り型 (Abstract return types, 旧称: RPIT, Return Position impl Trait) とは, Rust の関数の戻り値において, 戻り値型を明示せずにそれが満たすべき trait 境界 (trait とライフタイムを指定するやつ) だけを impl Trait
という構文で記述できる言語機能である.
例えば, 次のコード例を考えてみよう.
trait Show {
fn show(&self);
}
fn show_half(x: u32) -> impl Show {
struct ShowHalf(u32);
impl Show for ShowHalf {
fn show(&self) {
println!("{}", self.0 / 2);
}
}
ShowHalf(x)
}
このとき, show_half
関数は ShowHalf
構造体のオブジェクトを返しているが, その型は外から見えなくなっている (関数内で定義してある). しかし show_half
とその戻り値を利用するにあたっては, 要求を満たしてくれる何かしらの Show
を実装した型が返ってくるだけでよい. そこで戻り値型を impl Show
と記述することで, 具体的な型を明かさないままその性質を満たす実装を提供できる (もちろんコンパイル時に静的に具体的な型へ解決される).
こういう意味で, この impl Trait
は Trait
を要件としたゼロオーバーヘッドの抽象化を提供している. 大変便利なので, trait
の定義の中でも利用することができる.
trait IntoShow {
fn into_show(self) -> impl Show;
}
これは次のような匿名の関連型を用いた形へ脱糖される.
trait IntoShow {
type Anonymous: Show;
fn into_show(self) -> Self::Anonymous;
}
キャプチャ
さて, ここで抽象戻り型 impl Trait
にその関数の ジェネリック引数 (型引数, ライフタイム, コンパイル時定数, Self
など) が絡むと話が変わってくる. なぜなら, Trait
を実装している具体的な型 (hidden type, 拙訳: 隠匿型) がそれらのジェネリック引数を利用するかもしれないからである.
例えば, 文字列を借用して Show
を実装したオブジェクトに変換する次の実装が考えられる.
fn show_str'a>(s: &'a str) -> impl Show {
struct ShowStr'a>(&'a str);
impl'a> Show for ShowStr'a> {
fn show(&self) {
println!("{}", self.0);
}
}
ShowStr(s)
}
これは問題なく動作する. 注目すべき点として, 抽象戻り型 impl Show
の部分にはライフタイム引数 'a
が登場していない! にもかかわらず, この抽象戻り型は生存期間が 'a
であるオブジェクトを表している.
なぜなら, その隠匿型は利用可能なすべてのジェネリック引数を利用する可能性があるものとして扱われる (後述する指定をしない限りは). この「ジェネリック引数を利用する可能性がある」ことを キャプチャ という. つまり抽象戻り型はデフォルトで全てのジェネリック引数をキャプチャする.
全部キャプチャしてしまうことの問題点
しかし, 自動で全部キャプチャしてしまうために困ることがある. つまり問題ないはずの実装がコンパイルエラーにされてしまう. 例として次のコードを見てみよう.
trait Show {
fn show(&self);
}
fn void_ref_show'a>(_: &'a ()) -> impl Show {
struct VoidShow;
impl Show for VoidShow {
fn show(&self) {
println!("()");
}
}
VoidShow
}
fn foo(x: &mut ()) {
let show = void_ref_show(x);
*x = ();
show.show();
}
Precise Capturing
というわけで, 自動キャプチャで起こる過剰なキャプチャを削りたいことが稀にある. そのために追加された構文が use<.../>
による Precise Capturing (精密キャプチャ) である.
この言語機能は, 抽象戻り型で use
という trait 境界を使用することができる. これを指定すると, T1
, T2
, … の型だけをキャプチャするようになる.
use<.../>
を使うと, 先程のコードは次のように修正することでコンパイルできるようになる.
trait Show {
fn show(&self);
}
fn void_ref_show'a>(_: &'a ()) -> impl Show + use> {
struct VoidShow;
impl Show for VoidShow {
fn show(&self) {
println!("()");
}
}
VoidShow
}
fn foo(x: &mut ()) {
let show = void_ref_show(x);
*x = ();
show.show();
}
関数型プログラミングの文脈などでもうちょっと実践的な例もありそうだが, なかなか綺麗な例が思いつかないのでひとまずこれで筆を置くことにする.
まとめ
- 関数/メソッドの戻り型で使える抽象戻り型
impl Trait
という構文がある. - 抽象戻り型はその関数のジェネリック引数をすべてキャプチャする (消費するかのように振る舞う).
- ただし 2024 エディションから.
- キャプチャしないことを明示したいときに,
use<.../>
を使う.
参考文献
Views: 0