rust-random / rand

A Rust library for random number generation.
https://crates.io/crates/rand
Other
1.67k stars 432 forks source link

Add a way to derive `Standard` and `Uniform` distributions for a struct #1524

Open LikeLakers2 opened 2 weeks ago

LikeLakers2 commented 2 weeks ago

Background

What is your motivation?

rand offers the Standard and Uniform distributions, for making random samples of a primitive or struct. However, to implement them for a new type, the impls (impl Distribution<T> for Standard for Standard; impl SampleUniform for T and impl UniformSampler for TUniformSampler for Uniform) must currently be hand-written.

What type of application is this?

No specific type of application, though I write this after having to manually implement Uniform distribution support for the glam crate.

Feature request

I propose that derives be added for the Standard and Uniform distributions. When used on a struct where all fields already implement support for that type of distribution, it will generate the needed code to use Standard/Uniform distributions with that struct.

A derive for the Standard distribution would probably look like this (click the arrow to expand):

Standard distribution derive ```rust #[derive(StandardRand)] struct Vec3 { x: f32, y: f32, z: f32, } ``` This derive would generate code similar to the following: ```rust impl Distribution for Standard { fn sample(&self, rng: &mut R) -> Vec3 { Vec3 { x: rng.gen(), y: rng.gen(), z: rng.gen(), } } } ```

A derive for the Uniform distribution would be much the same, albeit with the addition of a struct being generated for the UniformSampler impl:

Uniform distribution derive ```rust #[derive(UniformRand)] struct Vec3 { x: f32, y: f32, z: f32, } ``` generates the following code: ```rust impl SampleUniform for Vec3 { type Sampler = Vec3UniformSampler; } struct Vec3UniformSampler { x_gen: Uniform, y_gen: Uniform, z_gen: Uniform, } impl UniformSampler for Vec3UniformSampler { type X = Vec3; fn new(low_b: B1, high_b: B2) -> Self where B1: SampleBorrow + Sized, B2: SampleBorrow + Sized, { let low = *low_b.borrow(); let high = *high_b.borrow(); // Asserts here are technically optional, since our fields are Uniforms, // but I'm including them anyways for the sake of the example. assert!(low.x < high.x, "Uniform::new called with `low.x >= high.x"); assert!(low.y < high.y, "Uniform::new called with `low.y >= high.y"); assert!(low.z < high.z, "Uniform::new called with `low.z >= high.z"); Self { x_gen: Uniform::new(low.x, high.x), y_gen: Uniform::new(low.y, high.y), z_gen: Uniform::new(low.z, high.z), } } fn new_inclusive(low_b: B1, high_b: B2) -> Self where B1: SampleBorrow + Sized, B2: SampleBorrow + Sized, { let low = *low_b.borrow(); let high = *high_b.borrow(); assert!(low.x < high.x, "Uniform::new_inclusive called with `low.x >= high.x"); assert!(low.y < high.y, "Uniform::new_inclusive called with `low.y >= high.y"); assert!(low.z < high.z, "Uniform::new_inclusive called with `low.z >= high.z"); Self { x_gen: Uniform::new_inclusive(low.x, high.x), y_gen: Uniform::new_inclusive(low.y, high.y), z_gen: Uniform::new_inclusive(low.z, high.z), } } fn sample(&self, rng: &mut R) -> Self::X { Self::X { x: self.x_gen.sample(rng), y: self.y_gen.sample(rng), z: self.z_gen.sample(rng), } } // sample_single() and sample_single_inclusive() are not included for the // sake of shortening the example. } ```

If I've missed any details, or if there's any questions you have for me regarding this suggestion, please don't hesitate to let me know.

dhardy commented 1 week ago

The rand lib used to have similar functionality which was deprecated (largely because of other changes which would have required significant revision to the macro and a lack of usage.) Interestingly, rand_derive now has 7 reverse-deps.

We could, then, add derive support. It would not be part of the rand crate but a separate crate.

It should be obvious that all such fields would be sampled independently, thus e.g. samples of Vec3 above would fill a cube not a sphere in 3D space. I wouldn't be surprised if some users fail to realise this however.

On the above snippets:

LikeLakers2 commented 1 week ago

I meant to post this yesterday: Shortly after making this feature request, I decided to start work on https://github.com/LikeLakers2/michis_rand_distr_derive, which is a proc-macro crate for deriving Standard and Uniform distributions for a struct.

However, its primary use is as derive-macro practice for myself - and in any case, it's not finished. Still, I wonder if it may be useful to some folks?


@dhardy Thanks for the comments. RE the snippets:

#[derive(UniformRand)] — yeah, not ideal having to use a fake name.

Just to make sure I understand: When you say "fake name", you're referring to UniformRand expanding into items that aren't called UniformRand?

struct Vec3UniformSampler — a "derive" macro should be hygenic and not dump extra items into the current scope. Unfortunately I don't think this is solvable, so I guess documenting this is about all we can do.

I do admit that a derive macro doing more than implementing a trait is odd. However, I'm not sure an attribute macro:

#[generate_uniform_support]
struct Vec3(f32, f32, f32);

or a function-like macro:

generate_uniform_support! {
    struct Vec3(f32, f32, f32);
}

would be any better, since several items (impl SampleUniform for MyType, struct MyTypeUniformSampler, and impl UniformSampler for MyTypeUniformSampler) need to exist for Uniform support to be possible.

benjamin-lieser commented 1 week ago

I can see the general use case for creating test instances of structs. I also don't think that people would care about the details of the resulting distribution (cube vs sphere), they just want something "random"

For this usecase a GetRandomInstance trait would probably ideal and it would also not interfere with what we already have. Option and Result could also implement this, because users would not care to much about the exact distributions of this trait.

Probably this would be functionality for it's own crate outside of the scope of rand. But I also see the appeal of rand being the one stop thing for everything random.

dhardy commented 1 week ago

would be any better

They're not. I guess what we could do is something like this:

[#derive_distr(Uniform, sampler=Vec3Uniform)]`
struct Vec3(f32, f32, f32);

in order to let users supply a name... but that is overly complicated (also less obvious what it does just reading it).

GetRandomInstance trait

You are referring to something like the old Rand trait? The limitation is that this only works for what we're probably now(#1297) calling StandardUniform, not also parametrised samplers like Uniform.

benjamin-lieser commented 1 week ago

You are referring to something like the old Rand trait? The limitation is that this only works for what we're probably now(#1297) calling StandardUniform, not also parametrised samplers like Uniform.

Yes. I think this limitation is fine if someone just wants something random. If you need more control you implement it yourself.