Random number generation is a fundamental technology that we use daily—encryption, simulation, approximation, testing, data selection, and more. Each of these applications has its own requirements for the random number generator (https://xkcd.com/221/). While encryption needs a generator that is as close to true randomness (https://www.random.org/) as possible, simulation, testing, and data selection may need to have reproducible samples drawn from a certain distribution.
Since there is no random generator in Rust's standard library, the rand crate is the way to go for many projects. Let's see how we can use it.
We can obtain randomness in just a few steps:
- Open a Terminal to create a new project using cargo new random-numbers --lib. Use VS Code to open the project directory.
- First, we need to add the rand crate as a dependency in Cargo.toml. Open it to add the following:
[dependencies]
rand = {version = "0.7", features = ["small_rng"]}
rand_distr = "0.2"
rand_pcg = "0.2"
- Since we are exploring how to use the rand library, we are going to add to the test module and implement three tests. Let's start by replacing the default content in src/lib.rs with some required imports:
#[cfg(test)]
mod tests {
use rand::prelude::*;
use rand::SeedableRng;
use rand_distr::{Bernoulli, Distribution, Normal, Uniform};
}
- Right underneath the imports (inside the mod tests scope), we are going to add the first test to check how Random Number Generators (RNGs) and Pseudo-Random Number Generators (PRNGs) work. To have predictable random numbers, we make every generator based on the first, which uses an array literal for initialization:
#[test]
fn test_rngs() {
let mut rng: StdRng = SeedableRng::from_seed([42;32]);
assert_eq!(rng.gen::<u8>(), 152);
let mut small_rng = SmallRng::from_rng(&mut rng).unwrap();
assert_eq!(small_rng.gen::<u8>(), 174);
let mut pcg = rand_pcg::Pcg32::from_rng(&mut rng).unwrap();
assert_eq!(pcg.gen::<u8>(), 135);
}
- Having seen regular (P)RNGs, we can move on to something more sophisticated. How about using these RNGs to operate on sequences? Let's add this test that uses PRNGs to do a shuffle and pick results:
#[test]
fn test_sequences() {
let mut rng: StdRng = SeedableRng::from_seed([42;32]);
let emoji = "ABCDEF".chars();
let chosen_one = emoji.clone().choose(&mut rng).unwrap();
assert_eq!(chosen_one, 'B');
let chosen = emoji.choose_multiple(&mut rng, 3);
assert_eq!(chosen, ['F', 'B', 'E']);
let mut three_wise_monkeys = vec!['1', '2', '3'];
three_wise_monkeys.shuffle(&mut rng);
three_wise_monkeys.shuffle(&mut rng);
assert_eq!(three_wise_monkeys, ['1', '3', '2']);
let mut three_wise_monkeys = vec!['1', '2', '3'];
let partial = three_wise_monkeys.partial_shuffle(&mut rng, 2);
assert_eq!(partial.0, ['3', '2']);
}
- As we stated in this recipe's introduction, RNGs can follow a distribution. Now, let's add another test to the tests module to draw random numbers that follow a distribution using the rand crate:
const SAMPLES: usize = 10_000;
#[test]
fn test_distributions() {
let mut rng: StdRng = SeedableRng::from_seed([42;32]);
let uniform = Uniform::new_inclusive(1, 100);
let total_uniform: u32 = uniform.sample_iter(&mut rng)
.take(SAMPLES).sum();
assert!((50.0 - (total_uniform as f32 / (
SAMPLES as f32)).round()).abs() <= 2.0);
let bernoulli = Bernoulli::new(0.8).unwrap();
let total_bernoulli: usize = bernoulli
.sample_iter(&mut rng)
.take(SAMPLES)
.filter(|s| *s)
.count();
assert_eq!(
((total_bernoulli as f32 / SAMPLES as f32) * 10.0)
.round()
.trunc(),
8.0
);
let normal = Normal::new(2.0, 0.5).unwrap();
let total_normal: f32 = normal.sample_iter(&mut rng)
.take(SAMPLES).sum();
assert_eq!((total_normal / (SAMPLES as f32)).round(), 2.0);
}
- Lastly, we can run the tests to see whether the test outputs positive results:
$ cargo test
Compiling random-numbers v0.1.0 (Rust-Cookbook/Chapter10/random-numbers)
Finished dev [unoptimized + debuginfo] target(s) in 0.56s
Running target/debug/deps/random_numbers-df3e1bbb371b7353
running 3 tests
test tests::test_sequences ... ok
test tests::test_rngs ... ok
test tests::test_distributions ... ok
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Doc-tests random-numbers
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
Let's see how it's done behind the scenes.
The rand crate has lived through several major version revisions since 2018 and several things have changed. In particular, the crate is now organized differently (https://rust-random.github.io/book/guide-gen.html), with several companion crates that contain implementations for lesser-used parts.
This is why, in step 2, we don't only import a single crate, even though they all share a single GitHub repository (https://github.com/rust-random/rand). The reason for this split was presumably to be compatible with the different requirements across the field.
RNGs represent—in short—a numeric sequence that is determined on the fly based on its predecessor. What is the first number though? It's called the seed and can be some literal (for reproducibility in tests) or as close to true randomness as possible (when not testing).
Popular seeds include seconds since 1 Jan 1970, entropy by the OS, user input, and more. The less predictable it is, the better.
In step 3, we set up the remaining code with some imports that we are using right away in step 4. There, we get into using different types of RNGs (https://rust-random.github.io/book/guide-rngs.html). The first is rand crate's StdRng, which is an abstraction over (as of this writing) the ChaCha PRNG (https://docs.rs/rand/0.7.0/rand/rngs/struct.StdRng.html), chosen for efficiency and cryptographic security. The second algorithm is SmallRng (https://docs.rs/rand/0.7.0/rand/rngs/struct.SmallRng.html), a PRNG chosen by the rand team that has great throughput and resource efficiency. However, since it is fairly easy to predict, the use cases have to be chosen carefully. The last algorithm (Pcg32) is a pick from the list of available PRNGs (https://rust-random.github.io/book/guide-rngs.html), which comes as part of a different crate.
In step 5, we work with sequences and choose from or shuffle through them. Functions include partial shuffling (that is, picking a random subset) and full, in-place shuffles, as wel...