Rustの乱数生成器はどんな型で受けるのが正解なのか
背景
Rustは静的型付言語であり、型に関しては厳密である一方で自由度を担保する仕組みも豊富にあります。その一つがtrait objectと呼ばれるものであり dyn TraitA
の様に型が宣言されます。
fn f(x: &dyn TraitA)
と書くとx
はTraitA
を実装した型の参照であればなんでもいいということになります。似たような概念に impl
というものがあり
fn f(x: &impl TraitA)
のように使われます。これはGenericsの別構文であり
fn f<A: TraitA>(x: &A)
と全く等価です。Genericsを使うとコンパイル時に型が確定し、型ごとに異なる関数が生成されるのに対し(static dispatch)、trait objectを使用すると本当に複数の実態型の関数が1つだけで実行時に動的に挙動が変わります(dynamic dispatch)。この両者の比較は本題ではないのでこれ以上は割愛します。
trait objectには制限があり、Traitがobject-safeである必要があります。このobject-safeを満たすうえで特に重要な項目がTraitがGenericsを含むmethodを持っているとobject-safeでなくなります。Rustでは乱数を使う際には算数生成器(RNG)のインスタンスを関数に渡すことが一般的です。
trait Sampler {
fn sample(&self, rng: &mut impl rand::Rng)
}
例えば上記のように複数のRNGの実装を使用できるようにするとこのSamplerというTraitはobject-safeではなくってしまいます。前置きがかなり長かったですがこれを考えるのが今回のテーマです。
結論
&mut dyn rand::RngCore
を使え。
まず最初に考えるのが &mut dyn rand::Rng
に変えることですが、これはできません。なぜならRng
自体がGenericsを含んだmethodをたくさん持っていてobject-safeではないからです。そこで登場するのがrand::RngCore
です。RngCore
もRng
のように様々なRNGが実装しているTraitです。というかRng
はRngCore
を継承しています。重要な点としてはRngCore
が持っているmethodはすべてGenericsを使用していないのでobject-safeであることです。一方でRngCore
は低レイヤーなメソッドしか実装しておらず。Rng
が実装しているgen_range
などが使えません。しかしこれも実は問題ありません。なぜならRngCore
自体にはこれらのmethodはありませんが、RngCore
はRng
を実装しているので使えてしまうんです。余談ですがこれは結構使えるテクニックだと思います。つまり以下のようなこと可能ということです。
trait TraitA {
fn do_something(&self);
}
trait TraitB: TraitA {
fn generic_method<T>(&self);
}
impl<K: TraitA> TraitB for K {
fn generic_method<T>(&self) {
println!("I can also do it");
}
}
struct StructA;
impl TraitA for StructA {
fn do_something(&self) {}
}
fn main() {
let a = StructA;
a.generic_method::<i32>();
}
そしてこれを見るとわかるようにTraitA
はTraitB
のmethodをすべてデフォルトで実装しておく必要があります。Rng
が提供している追加のmethodはすべて実際のstructの内容によらず、RngCore
のmethodから自動的に実装できるものだけということですね。
RNGの引数の型の指針
rand
crateのドキュメントを読むと通常は &mut (impl Rng + ?Sized)
の形で使えと書いてあります。?Sized
については他の解説に譲りますが、要は関数の引数の型を&mut (impl Rng + ?Sized)
にしておけば &mut dyn RngCore
も受けれるようになります。以下比較表を作りました。
引数型\変数型 | impl Rng | impl Rng + ?Sized | dyn RngCore | object-safe |
---|---|---|---|---|
impl Rng | OK | NG | NG | NO |
impl Rng + ?Sized | OK | OK | OK | NO |
dyn RngCore | OK | NG | OK | YES |
行が引数の型で列が実際に渡す変数の型です。つまり&mut (impl Rng + ?Sized)
はあらゆるパターンの変数を受けることができ、&mut (impl Rng)
はあらゆる引数パターンの関数に渡すことができるというわけです。これを考慮すると具体的に関数やTraitのmethodの引数は何を使用すればいいのかは以下のようになりました。
ユースケース | 引数の型 | 理由 |
---|---|---|
関数の引数で、内部で直接使用する場合 | &mut (impl Rng + ?Sized) |
受けれる範囲が最も広いため |
関数の引数で、内部でさらに別の関数に渡す場合 | 内部で呼ぶ関数の引数の型に合わせる | 内部の関数の要請を満たしつつ、受けれる変数の型の場合が最も多くなる |
Traitのmethodの引数で、object-safeにする必要がある場合 | &mut dyn RngCore |
object-safeにするにはこれ一択 |
Traitのmethodの引数で、object-safeにする必要が無く、できるだけ実装に幅を持たせたい | &mut impl Rng |
実際に実装する際に内部で他の関数に渡せる場合が最も多い |
Traitのmethodの引数で、object-safeにする必要が無く、できるだけ利用シーンを増やしたい | &mut impl Rng + ?Sized |
受けれる範囲が最も広いため |
Discussion