🔐

Rustの乱数生成器はどんな型で受けるのが正解なのか

2024/09/19に公開

背景

Rustは静的型付言語であり、型に関しては厳密である一方で自由度を担保する仕組みも豊富にあります。その一つがtrait objectと呼ばれるものであり dyn TraitA の様に型が宣言されます。

fn f(x: &dyn TraitA)

と書くとxTraitAを実装した型の参照であればなんでもいいということになります。似たような概念に 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です。RngCoreRngのように様々なRNGが実装しているTraitです。というかRngRngCoreを継承しています。重要な点としてはRngCoreが持っているmethodはすべてGenericsを使用していないのでobject-safeであることです。一方でRngCoreは低レイヤーなメソッドしか実装しておらず。Rngが実装しているgen_rangeなどが使えません。しかしこれも実は問題ありません。なぜならRngCore自体にはこれらのmethodはありませんが、RngCoreRngを実装しているので使えてしまうんです。余談ですがこれは結構使えるテクニックだと思います。つまり以下のようなこと可能ということです。

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>();
}

そしてこれを見るとわかるようにTraitATraitBの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