@dhardy, thank you so much for your work pulling together constraints and giving a strawman design!
The libs team had a preliminary meeting, with guest @briansmith, to talk through the known issues with rand
and also discussed a strawman design – sadly, this happened just before your initial proposal. However, there’s a lot of overlap with where we landed.
Here’s a sketch of what we discussed:
enum RngError { /* to be determined */ }
trait Rng {
fn fill(&mut self, dest: &mut [u8]) -> Result<(), RngError>;
// these defaulted methods allow for specialized, more
// efficient implementations
fn next_u8(&mut self) -> Result<(), RngError> { ... }
fn next_u16(&mut self) -> Result<(), RngError> { ... }
fn next_u32(&mut self) -> Result<(), RngError> { ... }
fn next_u64(&mut self) -> Result<(), RngError> { ... }
fn next_u128(&mut self) -> Result<(), RngError> { ... }
}
trait SecureRng: Rng {
// At the moment this is just a marker trait, but may grow defaulted methods
// over time.
}
/// Draws random values directly from the OS. Note that you can make as
/// many of these as you want, meaning that you can freely use the OS rng
/// on multiple threads.
struct OsRng;
// Provide some fast, seedable rng to use for e.g. simulation contexts
struct InsecureRng;
trait Rand<Dist> {
fn rand<R: Rng>(rng: &mut R, dist: Dist) -> Result<Self, RngError>;
}
impl Rand<RangeFull> for u64 {
fn rand<R: Rng>(rng: &mut R, dist: RangeFull) -> Result<u64, RngError> {
/* use `fill` in the obvious way */
}
}
impl Rand<Range<u64>> for u64 { /* ... */ }
// NB: there is *not* an impl for `RangeFull` on floats, which means you must use
// `random_range` as the convenience (and hence explicitly give the desired range)
struct HalfOpen01;
impl Rand<HalfOpen01> for f64 { /* ... */ }
struct Open01;
impl Rand<Open01> for f64 { /* ... */ }
struct Closed01;
impl Rand<Closed01> for f64 { /* ... */ }
// Convenience functions:
fn random<T: Rand<RangeFull>>() -> T {
// TBD: what's the right "DefaultRng" here?
T::rand(&mut DefaultRng, ..).unwrap()
}
fn random_range<R, T: Rand<R>>(r: R) -> T {
T::rand(&mut DefaultRng, r).unwrap()
}
This design pushes for maximal simplicity in the Rng
trait, but does allow for errors (important for cryptographic applications). The SecureRng
trait is something that @briansmith in particular believes is desirable in a crytpographic setting; I think given that it’s intended as a subtrait, we could punt on it at the beginning, until we have a better idea for the methods it should provide.
I think the biggest difference between this design and your proposal is that it retains the Rand
trait, but parameterizes it over a distribution type. The goals here are similar to yours, and I think the result is comparable to the gen
module idea, but would require fewer traits overall. I’m curious to hear what you think.
On a couple of the broader questions you raised:
-
Potential breakage. We didn’t talk about this extensively in the last libs team meeting, but my sense is that putting out a new major release (0.4) with significant changes is desirable and inevitable. The crate has long needed cleaning up.
-
RFC process. While I don’t think an RFC is required for moving forward with a proposal, I think it’d be a great step to take. RFCs have much greater visibility, so we’d have a better chance of hearing from all of the stakeholders. It would also mean we’d have a clear vision on record for the crate, which is something we’ve needed for a while. Finally, if we do want to move to 1.0 and move the crate into
rust-lang
proper, at that point an RFC is requried. This could essentially be that RFC, but we’d start by moving to 0.4 before finalizing the API.
In any case, @dhardy, the work you’ve been doing is great, and seems well-aligned with both the team’s thinking and stakeholder needs. If you want to lead the charge on turning it into a full RFC, I’m happy to help and provide feedback along the way!