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

Replace distribution::Sample with Distribution + polymorphism over Rng #256

Merged
merged 14 commits into from
Feb 20, 2018

Conversation

dhardy
Copy link
Member

@dhardy dhardy commented Feb 10, 2018

If #244 was controversial, this is even more so. There are many commits because there are many changes here, and these are bundled together to make Distribution::sample run-time polymorphic over Rng from the start without affecting Rand.

  1. Replace Sample and IndependentSample with Distribution
  2. Add a Uniform distribution and move all Rand implementations to it
  3. Switch Rng::gen() to use Uniform and deprecate Rand and rand_derive
  4. Merge SampleRng trait code (required to avoid having to type (&mut rng).gen() in all implementations)
  5. Change sample<R: Rng>(..) -> T to sample<R: Rng + ?Sized>(..) -> T

This PR is closely related to dhardy#83. We could potentially keep Rand and fix rand_derive to use Rand::rand(rng) not rng.gen(), or we could even keep Rand over Uniform but make change the method: rand<R: Rng + ?Sized>(..) -> T (breaking existing implementations, but it's still a smaller change for users than deprecating Rand like in this PR). I think I prefer approach since Uniform is a real distribution (uniformity, and maybe someday supporting currying of distributions) and avoids redundant feature overlap.

Comments please on whether you want rand to go in this direction!

@dhardy dhardy added the E-question Participation: opinions wanted label Feb 10, 2018
@dhardy
Copy link
Member Author

dhardy commented Feb 10, 2018

To be clear, there are alternatives to this PR:

  1. Not make Rng::gen() polymorphic over Rng (i.e. rng.gen() will not work with rng: &mut Rng, although (&mut rng).gen() appears to work — I don't understand why the latter works)
  2. Keep Rand but change its method to fn rand<R: Rng + ?Sized>(..). This breaks every implementation of Rand so users have to do a similar amount of fixing as with this PR.
  3. Add Uniform and keep Rand. It's not difficult (Rand is only deprecated in this PR), but leaves two different solutions to the same problem.

Also to be clear, Uniform has exactly the same functionality as Rand with slightly different syntax, e.g. see the Option<T> implementation:

impl<T> Distribution<Option<T>> for Uniform where Uniform: Distribution<T> {
    fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Option<T> {
        // UFCS is needed here: https://github.com/rust-lang/rust/issues/24066
        if rng.gen::<bool>() {
            Some(rng.gen())
        } else {
            None
        }
    }
}

@dhardy dhardy requested a review from KodrAus February 10, 2018 16:46
@KodrAus
Copy link

KodrAus commented Feb 12, 2018

@dhardy Just a ping to let you know I'll dig through this, but it might take a few days :)

@dhardy
Copy link
Member Author

dhardy commented Feb 12, 2018

@sbarral commented on Reddit that it may be worth keeping a distribution trait allowing mutable state since some genuine distribution sampling algorithms use it (e.g. Box-Muller generates two Gaussian samples simultaneously). Since this usage is the exception we agree that the primary distribution trait should not allow mutable state. I'm not sure what the best approach is here but see no reason why we can't leave this to later (i.e. another PR adding DistributionMut or StatefulDistribution or something) — none of the distributions implemented here use mutable state.

@KodrAus
Copy link

KodrAus commented Feb 15, 2018

You could also treat DistributionMut like a special case of Distribution:

pub trait DistributionMut<T> {
    fn sample<R: Rng + ?Sized>(&mut self, rng: &mut R) -> T;
}

impl<T, D: Distribution<T>> DistributionMut<T> for D {
    fn sample_mut<R: Rng + ?Sized>(&mut self, rng: &mut R) -> T {
        self.sample(rng)
    }
}

Sorry I've been out of the loop on rand for a while, but this looks like an improvement to me 👍

Something I'm not so sure on are the virtues of the SampleRng extension trait over provided methods on Rng. Is keeping those methods out of scope for backends worth the cognitive load on users? Are there other benefits to keeping Rng itself constrained to that single method? If there's been conversation on this previously I'd be happy to dig through it.

@dhardy
Copy link
Member Author

dhardy commented Feb 15, 2018

Thanks @KodrAus. There's some discussion in #244 (this PR just merges that one). As noted in the first post, it seems rng.gen() doesn't work if rng: &mut Rng (i.e. using dynamic dispatch) without adding the extension trait, though apparently (&mut rng).gen() does work — why? We could use that instead I guess; presumably it optimises out when using an rng of known type.


Regarding mutable (state-full) distributions, my conclusion from the discussion on reddit is that they won't be needed in the vast majority of cases, and since there's no reason the trait can't be defined externally, I'd rather leave it out of rand.

@KodrAus
Copy link

KodrAus commented Feb 15, 2018

Ahh I see, the Rng trait is object safe, but the extensions aren't. serde uses separate object-safe traits that rely on being able to forward object-safe variants of its methods to the non-object-safe originals. If we were going to follow something similar here then Rng would not be object-safe, and there'd be a separate trait like ObjectSafeRng you could use. That doesn't necessarily seem better than the Rng/SampleRng split now, it just slices the traits differently. So what we're doing here seems like a good approach to me.

@vks
Copy link
Collaborator

vks commented Feb 16, 2018

Regarding mutable (state-full) distributions, my conclusion from the discussion on reddit is that they won't be needed in the vast majority of cases, and since there's no reason the trait can't be defined externally, I'd rather leave it out of rand.

Also, if in the future there is some need for mutable distributions, this can still be implemented in rand without a breaking change.

@dhardy
Copy link
Member Author

dhardy commented Feb 19, 2018

Rebased after merge of #265.

@vks
Copy link
Collaborator

vks commented Feb 19, 2018

Shouldn't random be deprecated as well?

@dhardy
Copy link
Member Author

dhardy commented Feb 19, 2018

Why? It doesn't need Rand.

@vks
Copy link
Collaborator

vks commented Feb 19, 2018

Yes, but it has the same problems that Rand has. It doesn't communicate the distribution.

@dhardy
Copy link
Member Author

dhardy commented Feb 19, 2018

That is true. Why don't you open a new PR removing it so that it can be discussed there? It's independent of this PR.

random() is no different from rng.gen() in that respect though.

@vks
Copy link
Collaborator

vks commented Feb 19, 2018

Yeah, I'm not so happy with rng.gen() either. It is similar to fill though, except that it works for floats and char, where it is maybe better if the user has to specify the distribution. That it works with tuples is fine. I think uniform or sample_uniform would be a better name. The same applies to random.

Why don't you open a new PR removing it so that it can be discussed there?

Will do.

It's independent of this PR.

The deprecations are related, but it works as a follow-up I guess.

@dhardy
Copy link
Member Author

dhardy commented Feb 19, 2018

I get what you mean about gen not being ideal, but sample_uniform (or rng.sample(Uniform)) is a bit long really. I don't think gen is too bad.

@KodrAus
Copy link

KodrAus commented Feb 19, 2018

I like the idea of random being a low-barrier entry point for rand. Does it really need to communicate the distribution in the method name as well as the signature?

Copy link
Contributor

@pitdicker pitdicker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know you already merged this PR.

It looks really good, and I only have two questions.

let mut rng = ::test::rng(210);
for _ in 0..1000 {
norm.sample(&mut rng);
norm.ind_sample(&mut rng);
norm.sample(&mut rng);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this still a useful part of the test (here and all the similar tests that used ind_sample)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The duplication is not useful. I noticed this when merging these changes into dhardy/master but didn't fix yet.

As for the tests themselves — good question. I just opened #267.


macro_rules! float_impls {
($mod_name:ident, $ty:ty, $mantissa_bits:expr, $method_name:ident) => {
mod $mod_name {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why create modules here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I simply moved this code from rand_impls.rs. But I think it does have a purpose — SCALE is a local constant.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good reason. Then I feel free to remove them 😄

@dhardy dhardy mentioned this pull request Feb 24, 2018
33 tasks
pitdicker pushed a commit that referenced this pull request Apr 4, 2018
Replace distribution::Sample with Distribution + polymorphism over Rng
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
E-question Participation: opinions wanted
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants