🛞
Rustではクロージャを型制約にせず、別のtraitを挟んでほしい。
以下のLazy
などクロージャを型引数に持つ場合、F
のようにクロージャのトレイトを型制約にせず
struct Lazy<T, F: FnOnce() -> T> { /* 略 */ }
次のように、traitを一枚挟み、それを型引数の制約にしてほしい。
struct BetterLazy<T, F: Initializer<T>> { /* 略 */ }
trait Initializer<T> {
fn init(self) -> T;
}
impl<T, F: FnOnce() -> T> Initializer<T> for F {
fn init(self) -> T {
(self)()
}
}
理由
理由は型に名前がつかないからです。
クロージャのトレイトを使う場合
クロージャはコンパイラが型を生成しているため、プログラマがクロージャの型を書くことはできません。そのため、クロージャを型引数に持つ型も、クロージャを直接受け取る場合は、型を書くことができなくなります。
let cannot_write_type: Lazy<usize, /* ??? */> = Lazy::new(|| 0);
型が書けないと、その型を再利用することが難しくなります。例として、型の内部でLazy
を使いたい場合を考えます。
struct UseLazy {
inner: Lazy<usize, /* ??? */>
}
始めの例と同様に、クロージャの型は直接記述できないため、Box
などでくるむか関数ポインタをつかう必要があります。
// Boxなどでくるむか
struct UseLazy {
inner: Lazy<usize, Box<dyn FnOnce() -> usize>>
}
// もしくは関数ポインタを使う
struct UseLazy {
inner: Lazy<usize, fn() -> usize>
}
しかし、Box
などを使う場合は仮想関数呼び出しになってしまい、関数ポインタを使う場合は実行時の値が使えません。これでは、Rustの売りであるゼロコスト抽象化ができていません。
traitを一枚挟む場合
ここで、traitを一枚挟んだバージョンを考えます。
struct BetterLazy<T, F: Initializer<T>> { /* 略 */ }
trait Initializer<T> {
fn init(self) -> T;
}
型を書く必要があるときにはInitializer
を実装した型を作ればよくなります。
// 単に定数を持つだけのInitializer
struct ConstInit<T>(T);
impl<T> Initializer<T> for F {
fn init(self) -> T {
self.0
}
}
struct UseBetterLazy {
inner: BetterLazy<usize, ConstInit<usize>>
}
クロージャにblanket実装を与えれば、クロージャを使うこともできます。
impl<T, F: FnOnce() -> T> Initializer<T> for F {
fn init(self) -> T {
(self)()
}
}
fn use_closure() {
let lazy = BetterLazy::new(|| { 0 });
}
余談
今回の話は、クロージャのトレイトを自前で実装することが出来れば問題になりません。クロージャのトレイトを自前実装できるようにしたいというIssueは既にあり、nightlyであれば試すこともできるようです。
Discussion
maemonさんの実装とほとんど同じですが、以下のような実装はいかがでしょうか?
Initializer
トレイトに関する記述がより簡潔になり、読みやすいのではないでしょうか。コメントありがとうございます。
Taroさんの
Initializer
の定義ですと、Initializer
を実装できるのはFnOnce
を実装している型のみになります。いまのRustではFnOnce
を自前実装できないため、Initializer
も実装できなくなってしまいます。結果、Initializer
を実装するのはクロージャだけとなるため、型に名前がつかない問題が解決しなくなります。もちろん、Taroさんの
Initializer
の定義のような、Supertraitを実装する型にblanket実装を与える方法にも使い道はあり、Extensionとして利用できます。このパターンは、traitの本質的なメンバと利便性のためのメンバを切り離すことができ、必要な場合のみuseすればいいため割とよく使いますね。
返信ありがとうございます。
解決したい問題を私が勘違いしていました。
のようにただ別名をつけたいだけではなく、適切にtraitを実装すればなんでも渡せるようにしたいのであれば、確かに記事のような実装になりますね。