4

Consider the case where I have a function make_numbers which should create a string of random numbers, but where I want to decide at runtime (user input) what kind of random number generator should be used. To make it even more difficult, let's assume the make_numbers function to be generic over the type of numbers to be generated.

I wrote what I want to achieve with pseudo code, and I understand why this doesn't work. However, I don't know what an idiomatic way in Rust could look like to achieve this?

My naive ideas would be:

  1. Use Box<Rng>, but that doesn't work since Rng has generic functions.
  2. Use an enum over StdRng and XorShiftRng, but I cannot really think of a nice way to write this.

Can you give me some hints as to what a nice solution of this particular problem would look like?

Note: This question is not so much about the different match arms having different types (solutions could be Box or enum, as indicated above) - but how to apply these solutions in this case.

extern crate rand;

use rand::{Rng, SeedableRng, StdRng};
use rand::prng::XorShiftRng;
use std::string::String;
use rand::distributions::{Distribution, Standard};
use std::fmt::Display;

// Generic function that should work with any type of random number generator
fn make_numbers<T, R: Rng>(rng: &mut R) -> String 
    where T: Display, Standard: Distribution<T> 
{
    let mut s = String::new();
    for _i in 0..10 {
        s.push_str(format!("_{}", rng.gen::<T>()).as_str());
    }
    s
}

fn main() {
    let use_std = true; // -> assume that this will be determined at runtime (e.g. user input)

    // Pseudo code, will not work.
    let mut rng = match use_std {
        true => StdRng::from_seed(b"thisisadummyseedthisisadummyseed".to_owned()),
        false => XorShiftRng::from_seed(b"thisisadummyseed".to_owned())
    };

    let s = make_numbers::<u8>(&mut rng);

    // ... do some complex stuff with s ...

    print!("{}", s)
}
error[E0308]: match arms have incompatible types
  --> src/main.rs:24:19
   |
24 |       let mut rng = match use_std {
   |  ___________________^
25 | |         true => StdRng::from_seed(b"thisisadummyseedthisisadummyseed".to_owned()),
26 | |         false => XorShiftRng::from_seed(b"thisisadummyseed".to_owned())
   | |                  ------------------------------------------------------ match arm with an incompatible type
27 | |     };
   | |_____^ expected struct `rand::StdRng`, found struct `rand::XorShiftRng`
   |
   = note: expected type `rand::StdRng`
              found type `rand::XorShiftRng`
mrspl
  • 481
  • 1
  • 5
  • 10
  • Other recommendations: an `if..else` statement would be more readable than a `match` statement on a `bool`. The call to `make_numbers` also has to list both type parameters, even if just filled with `_` to let the compiler infer them: `make_numbers::(&mut rng)`. The implementation of `make_numbers` can become a one-liner with the iterator API: `(0..10).map(|_| format!("_{}", rng.gen::())).collect()`. Finally, don't forget to call `rustfmt` on your code. – E_net4 Oct 28 '18 at 11:21
  • @E_net4 I agree this is a duplicate, but there is a specific twist here, since the `Rng` trait isn't object safe, and the `RngCore` trait needs to be used instead. – Sven Marnach Oct 28 '18 at 11:50
  • Thanks for the comments. @E_net4, these are good suggestions! Regarding the duplicate, I agree that this might look like a duplicate, but as Sven mentioned, the boxing solution doesn't work in this case and my troubles are more about how to find a good solution when the underlying trait is not box-able (object-safe)... – mrspl Oct 28 '18 at 12:53

2 Answers2

6

You noticed yourself that you can't use Box<dyn Rng> since the Rng trait is not object-safe. The rand crate offers a solution for this, though: The foundation of each RNG is provided by the trait RngCore, which is object-safe, and Box<dyn RngCore> also implements Rng by means of these two trait implementations:

The first implementation makes sure that Box<dyn RngCore> is RngCore itself, while the second one implements Rng for all RngCore objects. In effect, you will be able to call all Rng methods on RngCore trait objects, and the implementation dynamically dispatches to the required RngCore methods under the hood.

Exploiting this, you can use the following code:

let mut rng: Box<dyn RngCore> = if use_std {
    Box::new(
        StdRng::from_seed(b"thisisadummyseedthisisadummyseed".to_owned())
    )
} else {
    Box::new(
        XorShiftRng::from_seed(b"thisisadummyseed".to_owned())
    )
};
let s = make_numbers::<u8, _>(&mut rng);
Sven Marnach
  • 574,206
  • 118
  • 941
  • 841
  • 1
    This is a nice solution to this precise problem, thanks. I hadn't really thought about using RngCore instead. In situations where such an object-safe trait is not provided, could I always implement a "dummy" trait and an implementation such as `impl Rng for R`? Or am I missing something? – mrspl Oct 28 '18 at 13:01
  • @mrspl The reason this works in this case is that all "required" methods are object-safe, and the non-object-safe methods in the `Rng` trait are all implemented in terms of the object-safe methods in the `RngCore` trait, i.e. the `Rng` trait only contains "provided" methods and no "required" methods. If your trait can be split up this way, you can use the same design, but you can never dynamically dispatch to a non-object-safe method. – Sven Marnach Oct 28 '18 at 18:08
1

I think you understand that the types of your match arms must be the same. (Otherwise, please refer to the suggested duplicate.)

One other option I see in your particular case is just calling make_numbers for each arm:

fn main() {
    let use_std = true;     
    let s = match use_std {
        true => make_numbers::<u8, _>(&mut StdRng::from_seed(b"thisisadummyseedthisisadummyseed".to_owned())),
        false => make_numbers::<u8, _>(&mut XorShiftRng::from_seed(b"thisisadummyseed".to_owned()))
    };
    print!("{}", s)
}

I see that this may not make sense if you have lots of additional parameters into make_numbers.

In such cases, I resorted to macros:

fn main() {
    let use_std = true;  
    macro_rules! call_make_numbers(($t:ty, $rng:ident, $str:expr) => {
        make_numbers::<$t, _>(&mut $rng::from_seed($str.to_owned()))
    });
    let s = match use_std {
        true => call_make_numbers!(u8, StdRng, b"thisisadummyseedthisisadummyseed"),
        false => call_make_numbers!(u8, XorShiftRng, b"thisisadummyseed"),
    };
    print!("{}", s)
}
phimuemue
  • 34,669
  • 9
  • 84
  • 115