Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
LikeLakers2 opened this issue Nov 6, 2024 · 5 comments
Open

Comments

@LikeLakers2
Copy link

LikeLakers2 commented Nov 6, 2024

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
#[derive(StandardRand)]
struct Vec3 {
	x: f32,
	y: f32,
	z: f32,
}

This derive would generate code similar to the following:

impl Distribution<Vec3> for Standard {
	fn sample<R: Rng + ?Sized>(&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
#[derive(UniformRand)]
struct Vec3 {
	x: f32,
	y: f32,
	z: f32,
}

generates the following code:

impl SampleUniform for Vec3 {
	type Sampler = Vec3UniformSampler;
}

struct Vec3UniformSampler {
	x_gen: Uniform<f32>,
	y_gen: Uniform<f32>,
	z_gen: Uniform<f32>,
}

impl UniformSampler for Vec3UniformSampler {
	type X = Vec3;

	fn new<B1, B2>(low_b: B1, high_b: B2) -> Self
	where
		B1: SampleBorrow<Self::X> + Sized,
		B2: SampleBorrow<Self::X> + 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<B1, B2>(low_b: B1, high_b: B2) -> Self
	where
		B1: SampleBorrow<Self::X> + Sized,
		B2: SampleBorrow<Self::X> + 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<R: Rng + ?Sized>(&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
Copy link
Member

dhardy commented Nov 8, 2024

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:

  • #[derive(UniformRand)] — yeah, not ideal having to use a fake name. I'd be tempted to provide an impl-tools::autoimpl implementation instead just because that supports paths. (I wrote impl-tools because standard custom derives don't provide enough flexibility, though I don't think we'd have any use for its other features like ignore field / using field / enum support here.) Maybe just #[derive(rand_distr_Uniform)] is good enough.
  • 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.
  • Plenty of times the derive will fail because a (generic) field type does not support Uniform / Standard. We may remove support for Option in the next release, and we won't try to support a vast number of generic types. This does limit the usefulness of a derive macro, though not remove it's uses completely.

@LikeLakers2
Copy link
Author

LikeLakers2 commented Nov 8, 2024

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
Copy link
Collaborator

benjamin-lieser commented Nov 8, 2024

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
Copy link
Member

dhardy commented Nov 8, 2024

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
Copy link
Collaborator

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants