0

I'm new to Rust, and still getting used to all the idioms in the language. I want to create an object that uses a random distribution, so I imported the rand and rand_distr crates, and am defining the object as follows:

pub struct Bandit<const K: usize> {
  levers: [f32; K],
  dist: Box<dyn Distribution<f32>>,
}

The Distribution type is defined in the rand crate here.

The above code is giving me the following error:

error[E0038]: the trait `rand_distr::Distribution` cannot be made into an object
  --> src/testbed.rs:8:15
   |
8  |     dist: Box<dyn Distribution<f32>>,
   |               ^^^^^^^^^^^^^^^^^^^^^ `rand_distr::Distribution` cannot be made into an object
   |
note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>
  --> /Users/brendan/.cargo/registry/src/github.com-1ecc6299db9ec823/rand-0.8.5/src/distributions/distribution.rs:37:8
   |
37 |     fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> T;
   |        ^^^^^^ the trait cannot be made into an object because method `sample` has generic type parameters

My workaround for this is to define my own enum which I can map to a distribution later:

pub enum DistType {
  Normal { mean: f32, variance: f32 },
  Uniform,
}

impl DistType {
  pub fn distribution(&self) -> Box<impl Distribution<f32>> {
    match self {
        DistType::Normal { mean, variance } => /* return normal */,
        DistType::Uniform => /* return uniform */,
    }
  }
}

However, I don't like this solution for the reasons:

  • If I can't store the distribution object, it means I have to create one whenever I need to take random samples. I suspect this will create some performance issues.
  • If possible, would like to use dyn instead of impl. There may be many more of these types of distributions.
  • I would prefer to avoid creating custom types like the enum I defined above because I would like to avoid unnecessary indirection.

Is there a way to persist dyn Trait objects that are not object safe?

  • Tried dyn vs impl keywords. Reviewed the semantics of what those error and definitely do not want impl in this case
  • Tried searching for ways to persist traits that are not object safe and could not find anything
  • Looked for examples in the rand_distr crate, but all the examples create local distribution variables and use them immediately.
0

1 Answer 1

2

Generally, you cannot have dyn Trait if one of your methods contains a generic parameter. However, there is a pattern to handle this case, and it is called type erasure.

The idea is that we create a new trait, ErasedDistribution, that in its sample() method, instead of taking a generic parameter R: Rng we will take a &mut dyn Rng. As every Rng is also &mut dyn Rng we can implement our type for every Distribution out there. However, as &mut dyn Rng is also Rng we can also implement Distribution for dyn ErasedDistribution. And now we can store a Box<dyn ErasedDistribution<f32>>, and act as if it was Box<dyn Distribution<f32>> - we can store every Distribution inside it, and we can call Distribution's methods (such as sample_iter()) on it or pass it to code that expects a Distribution.

The first step is to create our trait:

use rand::distributions::Distribution;

pub trait ErasedDistribution<T> {
    fn sample(&self, rng: &mut dyn rand::RngCore) -> T;
}

Rng is not object safe, so we cannot pass &mut dyn Rng. However, we got lucky: Rng has RngCore as a supertrait, and RngCore is object-safe. Furthermore, there is a blanket implementation of Rng for every RngCore, so we can just take &mut dyn RngCore.

If this was not the case, our erasure work would be more involved (but not impossible): we would also had to create a ErasedRng trait, and take &mut dyn ErasedRng as a parameter.

The next step is to create a blanket implementation, so every Distribution will also implement ErasedDistribution:

impl<T, D: Distribution<T> + ?Sized> ErasedDistribution<T> for D {
    fn sample(&self, rng: &mut dyn rand::RngCore) -> T {
        <Self as Distribution<T>>::sample(self, rng)
    }
}

The final step is to implement Distribution for ErasedDistribution. I also implemented it for &ErasedDistribution, because Distribution's methods take self by move and we can't move dyn ErasedDistribution (we aren't covered by the blanket implementation Distribution for &Distribution because it is limited to Sized types - I sent a PR to relax that):

impl<T> Distribution<T> for dyn ErasedDistribution<T> {
    fn sample<R: rand::Rng + ?Sized>(&self, mut rng: &mut R) -> T {
        <dyn ErasedDistribution<T> as ErasedDistribution<T>>::sample(self, &mut rng)
    }
}

impl<T> Distribution<T> for &'_ dyn ErasedDistribution<T> {
    fn sample<R: rand::Rng + ?Sized>(&self, mut rng: &mut R) -> T {
        <dyn ErasedDistribution<T> as ErasedDistribution<T>>::sample(&**self, &mut rng)
    }
}

Now you can store a Box<dyn ErasedDistribution<f32>>. You may need to take a reference explicitly before calling Distribution's methods, because Rust defaults to calling them on dyn ErasedDistribution instead of &dyn ErasedDistribution.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.