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 fill and try_fill methods to Rng #247

Merged
merged 2 commits into from
Feb 21, 2018
Merged

Conversation

dhardy
Copy link
Member

@dhardy dhardy commented Jan 29, 2018

This was suggested a couple of times. It's perhaps more useful with #244 since then fill_bytes and try_fill_bytes are not available in SampleRng.

I'm not entirely happy with the code organisation; this adds a generic-sounding trait name, but perhaps it should be renamed AsByteSliceMut and not have fn to_le to meet what would be expected from the name. From our point of view those are needless complications however.

This also adds more unsafe code but I don't think that's avoidable.

@pitdicker
Copy link
Contributor

I have been working on something similar, but with a different approach. pitdicker@bd44cd2 is the latest try. Every type there has a default implementation that uses the same method as gen, and integer slices have a custom implementation that uses fill_bytes directly. Worth comparing?

@dhardy
Copy link
Member Author

dhardy commented Jan 29, 2018

Interesting; you push more of the implementation down into the trait. Your version also supports any type supporting the Default distribution, at the cost of requiring per-element copy, then specialises for integer slices; however doesn't support error-forwarding via try_fill_bytes (which would require a separate trait since this is part of the trait implementation).

I'm not sure which I prefer; I'm also not sure how useful the error forwarding would be in practice (with many generators being infallible, and for the rest a single "try generating a random word" check after initialisation would suffice to catch most errors).

@pitdicker
Copy link
Contributor

pitdicker commented Jan 29, 2018

Good summary!

For some things like distributions returning Results just doesn't make sense. I wish we had some solution for the few cases where distributions are combined with a fallible RNG. Just an idea, but what do you think about this?

To add a wrapper like FallibleRng that reimplements next_u32, next_u64 and fill_bytes by wrapping try_fill bytes. If there is some error, store the error in an extra field. And to add a functions like is_ok() and get_result().

Sorry for bringing up a different issue though...

@dhardy
Copy link
Member Author

dhardy commented Jan 30, 2018

No, I don't think much of that. If the wrapper is implementing the same next_u32 etc. then it can only handle errors from its source by (a) panicking (roughly the same as now) or (b) by returning some non-random data and expecting a user to call is_ok() afterwards. Also you miss the implicit error handling we have now in OsRng::fill_bytes.

@pitdicker
Copy link
Contributor

Are you ready to make a choice between the two options? I like mine more 😄. But my variant does nothing that can not be achieved in some other way just as easy and fast. While yours can also pass on errors, so that seems like the better choice.

@dhardy
Copy link
Member Author

dhardy commented Feb 1, 2018

I added a second commit inspired by @pitdicker's version.

There is however a significant difference: @pitdicker's code also supports any type supported by the default distribution, e.g. arrays, slices and floating-point numbers, so for example it could fill an array with FP numbers in the range [0, 1). My code in this PR only supports byte-copy to slices of integers (it can be extended to arrays and tuples similar to Rand).

So, what should this do?

let mut array = [0f32; 100];
rng.fill(&mut array);  // or &mut array[..]
  1. Create an array of 100 × f32 each in range [0, 1)
  2. Fill with random bytes, allowing all f32 values including NaN
  3. Not be supported

Pretty obviously, option (2) has very little use (and would not be safe with all types). Option (1) is well-defined but appears counter-intuitive (or maybe that's just me) — it requires accessing elements individually and using a distribution, instead of just a block-copy. I prefer (3) but others may prefer (1)?

src/lib.rs Outdated
/// Trait for casting types to byte slices
pub trait AsByteSlice {
/// Return a mutable reference to self as a byte slice
fn as_byte_slice<'a>(&'a mut self) -> &'a mut [u8];
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't know for sure (still a novice and no computer right now), but is it possible to end up with two mutable references to the same memory after this function?

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 reference passed in will be locked until after the result is destroyed thanks to Rust's lifetime analysis. It's actually hard to get this wrong in Rust.

src/lib.rs Outdated
/// Return a mutable reference to self as a byte slice
fn as_byte_slice<'a>(&'a mut self) -> &'a mut [u8];
/// Perform byte-swapping on BE platforms
fn to_le(&mut self);
Copy link
Contributor

Choose a reason for hiding this comment

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

This does not really need to be part of the trait. Wouldn't it be easier to just write the conversion loop in fill and try_fill?

Copy link
Member Author

Choose a reason for hiding this comment

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

Okay, then we can also standardise the naming.

Copy link
Member

Choose a reason for hiding this comment

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

It will require to use more complex trait bounds, not only you'll need trait for iteration, but also for to_le method as well.

@pitdicker
Copy link
Contributor

Looks good to me!

@dhardy
Copy link
Member Author

dhardy commented Feb 6, 2018

@burdges @newpavlov you commented on this topic before; any thoughts on this PR or @pitdicker's version? I'm not sure whether to add this, some other variant of fill, or nothing like this.

Copy link
Member

@newpavlov newpavlov left a comment

Choose a reason for hiding this comment

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

I don't mind this addition, but I think we better to list primitive types (i.e. u16, u32, etc.) in the methods documentation and add to AsByteSliceMut note that its main usage is for fill and try_fill methods.

src/lib.rs Outdated
let slice = dest.as_byte_slice_mut();
self.fill_bytes(slice);
for mut x in slice {
x.to_le();
Copy link
Member

Choose a reason for hiding this comment

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

If I am reading this correctly this line does not do anything, because to_le for u8 should be no-op. We'll need to add to_le method to AsByteSliceMut trait.

Copy link
Member Author

Choose a reason for hiding this comment

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

You're right. Actually, that's what I had originally.

Copy link
Contributor

Choose a reason for hiding this comment

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

Good catch, but shouldn't it just loop over dest?

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 don't see a trait supporting to_le though so it has to be done per implementation.

Copy link
Member

Choose a reason for hiding this comment

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

Then you'll need to add another bound to allow you iterate mutably over dest. I think it's easier to add a method to AsByteSliceMut.

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 wonder if eventually we'll get a suitable trait; for now it doesn't look like it (also nothing in the num crate I can see).

src/lib.rs Outdated
fn to_le(&mut self) {
for mut x in self {
x.to_le();
}
Copy link
Member

@newpavlov newpavlov Feb 6, 2018

Choose a reason for hiding this comment

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

I think this will not work. You need to write:

for x in self {
    *x = x.to_le();
}

Copy link
Member Author

Choose a reason for hiding this comment

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

Well spotted. There's a unit test already but I we need to run CI on a BE platform...

@dhardy dhardy force-pushed the fill_rng branch 2 times, most recently from 80696ec to 746a7aa Compare February 7, 2018 12:19
@dhardy
Copy link
Member Author

dhardy commented Feb 7, 2018

@newpavlov I added some documentation.

Because this works by filling dest with random bytes, I wonder if fill_bytes is more appropriate — we could replace the current fill_bytes / try_fill_bytes with these functions instead. That should be fully compatible with users of fill_bytes but not existing Rng implementations (though those will require updating anyway with the next rand release). It requires that each implementation calls dest.as_byte_slice_mut() and (if appropriate) dest.to_le(), i.e. a tiny increase in implementation code (with more flexibility for Endianness handling).

An advantage of doing it that way would be that we could still add a more generic fn fill similar to @pitdicker's version later if we want. Thoughts?

@dhardy dhardy added the E-question Participation: opinions wanted label Feb 7, 2018
@vks
Copy link
Collaborator

vks commented Feb 16, 2018

An advantage of doing it that way would be that we could still add a more generic fn fill similar to @pitdicker's version later if we want. Thoughts?

Yes, I think this reasonable. Also, fill for floats can be implemented more efficiently, because less than 32/64 bits are needed per float (see the dSFMT random number generator).

@dhardy
Copy link
Member Author

dhardy commented Feb 16, 2018

Regarding floats, see this discussion. I'd prefer to leave them out of this discussion since there are far too many possible trade-offs.

@vks
Copy link
Collaborator

vks commented Feb 16, 2018

I agree, which is why I think it is a good idea to only have fill_bytes without support for floats.

@dhardy
Copy link
Member Author

dhardy commented Feb 19, 2018

My previous idea of replacing fill_bytes and try_fill_bytes with these functions doesn't work, and the new extension trait split Rng: RngCore emphasises why: the base functions used in RngCore must not be generic so that RngCore can be object-safe (and without removing any functionality).

We could still use those names for the new functions and rename or shadow the old functions, but I don't think either is a good idea, so fill and try_fill will have to do (unless we prefer fill_slice etc.).

@dhardy
Copy link
Member Author

dhardy commented Feb 19, 2018

Rebased on top of master (with new RngCore).

@vks
Copy link
Collaborator

vks commented Feb 19, 2018

Looks good to me! I would consider explicitly mentioning that the bytes are uniformly distributed.

@dhardy
Copy link
Member Author

dhardy commented Feb 19, 2018

I have a commit adding support for arrays (up to length 32) using recursive expansion like Rand, but I noticed it doubles the compile time — from:

real    0m2.180s
user    0m2.666s
sys     0m0.116s

to:

real    0m3.654s
user    0m4.637s
sys     0m0.144s

but with some re-thinking, I got this down to:

real    0m2.278s
user    0m2.791s
sys     0m0.100s

Once support for generic constants lands we should be able to support arrays properly (without all the macro recursion nonsense).

Usage of recursive macros appears to have some compile time
hit, but with a single macro and generic impl it's not too much
(directly recursing for each type is far worse).
@dhardy
Copy link
Member Author

dhardy commented Feb 20, 2018

Rebase after merge of #256.

@dhardy dhardy merged commit d9bde36 into rust-random:master Feb 21, 2018
@dhardy dhardy deleted the fill_rng branch March 14, 2018 16:39
pitdicker pushed a commit that referenced this pull request Apr 4, 2018
Add fill and try_fill methods to 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