Randomness: std has none¶
Level: 101 → 201 · for newcomers
One line: Rust ships no random number generator, so rand is the first dependency nearly every tutorial adds — and the two names the Rust Book teaches, thread_rng() and gen_range(), no longer exist, which is why the Book's guessing game does not compile against a rand you install today.
The guessing game, working, on rand 0.10.2:
use std::io;
fn main() {
let secret_number: u32 = rand::random_range(1..=100); // no handle, no import
println!("Guess the number! (1–100)");
let mut guess = String::new();
io::stdin().read_line(&mut guess).expect("Failed to read line");
println!("You guessed: {}", guess.trim()); // You guessed: 42
}
What was renamed¶
rand 0.9 renamed the API and 0.10 kept the new names. The Rust Book still teaches the old ones, so a reader following it gets a compile error on the first line they write:
error[E0425]: cannot find function `thread_rng` in crate `rand`
--> src/main.rs:4:31
|
4 | let secret_number = rand::thread_rng().gen_range(1..=100);
| ^^^^^^^^^^ not found in `rand`
| The Book writes | Current rand |
Notes |
|---|---|---|
rand::thread_rng() |
rand::rng() |
the thread-local generator |
rng.gen_range(1..=100) |
rng.random_range(1..=100) |
method on a generator |
rng.gen::<T>() |
rng.random::<T>() |
|
use rand::Rng; |
use rand::RngExt; |
see below — this one bites second |
| — | rand::random_range(1..=100) |
free function; no handle, no import |
The second surprise: the trait is RngExt¶
Fixing the function name alone gets you a different error, because random_range moved off Rng onto a supertrait extension:
error[E0599]: no method named `random_range` found for struct `ThreadRng` in the current scope
|
7 | let secret_number = rand::rng().random_range(1..=100);
| ^^^^^^^^^^^^
|
= help: items from traits can only be used if the trait is in scope
rand 0.10 declares pub trait RngExt: Rng, and the range and typed-value methods live on the extension. So use rand::Rng; compiles and then fails at the call — the most confusing possible order. Three spellings all work; pick by how many draws you need:
use rand::RngExt;
fn main() {
let one: u32 = rand::random_range(1..=100); // one draw, no import needed
let mut rng = rand::rng(); // a handle, for many draws
let a: u32 = rng.random_range(1..=100);
let b: bool = rng.random();
println!("{one} {a} {b}"); // 61 88 true (different every run)
}
If you only draw once, the free function is the whole answer and the use line is noise. Add the handle when you are drawing in a loop — it avoids re-fetching the thread-local generator each time.
Pin it, and read the version you got. rand = "0.10" is a range, and this is a crate that has renamed its API twice; adding a dependency is the page on what that number actually promises. cargo tree | head -3 tells you which one resolved, and it is the first thing to check when a tutorial's code will not build.
Why std has none¶
Randomness is a policy question wearing an API. Cryptographic or fast? Seeded for reproducibility or drawn from the OS? Which distribution? A standard library that picked one would be wrong for the others and could never change its mind, since std is stable forever. So it ships the pieces that need no policy — RandomState and DefaultHasher are OS-seeded, but they are hashing tools, not a generator — and leaves the rest to a crate that can version its answers. rand renaming its API twice is that plan working, not failing.
Seeded means reproducible, and that is the useful half¶
"Random" in a program means unpredictable if you do not know the seed — not unrepeatable. Two generators with the same seed emit the same numbers forever, which is what makes a randomized program testable at all:
Verified output of randomness.rs — regenerated by tools/run_examples.py, never hand-typed.
1. A seed determines the whole sequence
seed 2026, generator A: [261, 543, 261, 100]
seed 2026, generator B: [261, 543, 261, 100]
identical: true
'Random' here means unpredictable-if-you-do-not-know-the-seed,
not unrepeatable. Same seed, same numbers, forever.
2. A different seed is a different sequence
seed 2027: [716, 966, 484, 377]
differs from seed 2026: true
3. The guessing game's secret number
secret = rng.range(1, 100) = 61
in 1..=100: true
With `rand` this line is `rand::random_range(1..=100)` and the
seed comes from the operating system, so it is different each run.
4. Why `% n` is not good enough, counted exactly
Take 4 random bits (16 equally likely patterns) and map them to
1..=10 with `1 + bits % 10`. Count how many patterns reach each:
1:2 2:2 3:2 4:2 5:2 6:2 7:1 8:1 9:1 10:1
Six of the ten outcomes are twice as likely as the other four.
That is modulo bias, and it is exactly why you use a library:
`rand` rejects and redraws the values that would skew the result
instead of folding them back in.
Total patterns: 16 (and 16 is not a multiple of 10 — that is the whole cause)
5. What std actually gives you
No RNG at all. std has `RandomState` (for HashMap) and `DefaultHasher`,
which are seeded from the OS but are hashing tools, not a generator API.
The answer in real code is the `rand` crate. The answer in THIS library's
examples is a seeded generator like the one above, because every example
here is compiled with bare `rustc` and diffed against a recorded answer
key — a value that changed per run would fail the check every time.
Section 4 above is the reason to reach for the library rather than write % n yourself. The bias is a property of the arithmetic, not of the generator, so a better source of bits cannot fix it — rand rejects and redraws the draws that would skew the result.
Every example in this library uses a hand-rolled seeded generator like that one rather than rand, and the reason is structural: examples here are compiled with bare rustc and diffed against a recorded answer key, so a number that changed per run would fail the check every time. That constraint is worth borrowing. Seed your own tests.
If you are coming from another language¶
Python. random is in the standard library, so the shape is familiar and the split is not:
| Python | Rust | |
|---|---|---|
| one draw | random.randint(1, 100) |
rand::random_range(1..=100) |
| a generator you hold | random.Random(seed) |
StdRng::seed_from_u64(seed) |
| the implicit global | module-level random.* |
rand::rng(), thread-local |
| seeding the global | random.seed(2026) |
no equivalent — take a handle instead |
| cryptographic | a different module, secrets |
a marker trait, CryptoRng, on the same API |
The seeded handle in row two needs two imports, and forgetting SeedableRng is the same shape of error as the RngExt one above:
use rand::{RngExt, SeedableRng};
use rand::rngs::StdRng;
fn main() {
let mut rng = StdRng::seed_from_u64(2026);
let x: u32 = rng.random_range(1..=1000);
println!("{x}"); // the same number on every run, on every machine
}
The last two rows are the real differences. Python's module-level functions share one hidden generator you can reseed from anywhere, which is convenient and is also why a library reseeding it can silently change your results; Rust's thread-local has no reseed door, so reproducibility means holding your own StdRng. And secrets versus random is a choice you make by remembering to — CryptoRng is a bound the compiler can check for you.
ABAP. The CL_ABAP_RANDOM family is the closest thing, and it maps almost one-to-one onto Rust's handle form: you create an instance with a seed and call it for values, rather than calling a free function. The idiom of seeding from the current time is the same idea as Rust's rand::rng() drawing from the OS — an unrepeatable starting point on purpose. What changes is the guarantee around it: nothing in ABAP stops you from mixing a seeded generator and an unseeded one in the same report and wondering later why a run did not reproduce, whereas in Rust the two are different types and the seeded one has to be passed explicitly to everything that uses it. That is more typing and it is also the audit trail.
Practice¶
Seed it, prove it, measure the bias, then remove it.
Write a small generator — six lines of xorshift is plenty — that hands out 4 bits at a time, so its whole output space is 16 values and every claim below can be counted rather than sampled.
- Show that two instances built from the same seed emit the same first five values. Use
assert_eq!, not your eyes. - Fold a draw into
1..=nwith1 + bits % n. Do it forn = 8and forn = 10, counting over all 16 patterns. Which one is fair, and what is the rule? - Repeat for
n = 3andn = 6. No seed appears in this step or the last — why does that matter for the diagnosis? - Rewrite the fold to reject the draws that would skew it, and count again to prove the skew is gone. What did it cost?
- For each
nabove, what fraction of draws gets thrown away? Why is that fraction irrelevant with a real 64-bit generator?
Solution
randomness_kata.rs in full — pasted here by tools/run_examples.py from the file CI compiles and runs.
//! Kata solution: seed it, prove it, measure the bias, then remove the bias.
//!
//! rustc --edition 2024 randomness_kata.rs -o /tmp/rndk && /tmp/rndk
struct Xorshift64(u64);
impl Xorshift64 {
fn new(seed: u64) -> Self {
Xorshift64(if seed == 0 { 0x9E37_79B9_7F4A_7C15 } else { seed })
}
fn next_u64(&mut self) -> u64 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.0 = x;
x
}
/// Four bits at a time, so the whole output space is 16 values and every
/// claim below can be COUNTED rather than sampled.
fn nibble(&mut self) -> u32 {
(self.next_u64() & 0xF) as u32
}
}
/// Fold a 4-bit draw into 1..=n. Biased whenever 16 % n != 0.
fn naive(bits: u32, n: u32) -> u32 {
1 + bits % n
}
/// Reject the draws that would skew it, and ask for another. This is what
/// `rand`'s `random_range` does; the only cost is an occasional extra draw.
fn rejecting(bits: u32, n: u32) -> Option<u32> {
let limit = (16 / n) * n; // the largest multiple of n that fits in 0..16
if bits < limit { Some(1 + bits % n) } else { None }
}
/// Exact distribution over ALL 16 patterns — no sampling, no seed involved.
fn tally(n: u32, reject: bool) -> Vec<(u32, u32)> {
let mut counts = vec![0u32; (n + 1) as usize];
for bits in 0..16u32 {
let hit = if reject { rejecting(bits, n) } else { Some(naive(bits, n)) };
if let Some(v) = hit {
counts[v as usize] += 1;
}
}
(1..=n).map(|v| (v, counts[v as usize])).collect()
}
fn show(label: &str, rows: &[(u32, u32)]) {
let body: Vec<String> = rows.iter().map(|(v, c)| format!("{v}:{c}")).collect();
let hits: u32 = rows.iter().map(|(_, c)| c).sum();
let lo = rows.iter().map(|(_, c)| *c).min().unwrap();
let hi = rows.iter().map(|(_, c)| *c).max().unwrap();
println!(" {label:<22} {} ({hits}/16 patterns used, spread {lo}..{hi})", body.join(" "));
}
fn main() {
println!("1. Same seed, same sequence — asserted, not eyeballed");
let mut a = Xorshift64::new(7);
let mut b = Xorshift64::new(7);
let sa: Vec<u32> = (0..5).map(|_| a.nibble()).collect();
let sb: Vec<u32> = (0..5).map(|_| b.nibble()).collect();
println!(" A: {sa:?}");
println!(" B: {sb:?}");
assert_eq!(sa, sb, "a seeded generator must be reproducible");
println!(" assert_eq!(A, B) passed — reproducibility is a property you can TEST,");
println!(" which is the reason a library exposes seeding at all.\n");
println!("2. A range that divides the output space, and one that does not");
println!(" The generator emits 4 bits: 16 equally likely patterns.");
show("1..=8 (16 % 8 == 0)", &tally(8, false));
show("1..=10 (16 % 10 == 6)", &tally(10, false));
println!(" `1..=8` is already fair: every outcome gets exactly 2 patterns.");
println!(" `1..=10` is not, and the six low outcomes are twice as likely.\n");
println!("3. The bias is a property of the ARITHMETIC, not of the generator");
show("1..=3 (16 % 3 == 1)", &tally(3, false));
show("1..=6 (16 % 6 == 4)", &tally(6, false));
println!(" No seed appears in section 2 or 3. Every pattern is counted once,");
println!(" so a better generator cannot fix this — only better arithmetic can.\n");
println!("4. Reject and redraw, and the skew goes to zero");
show("1..=10 rejecting", &tally(10, true));
show("1..=3 rejecting", &tally(3, true));
show("1..=6 rejecting", &tally(6, true));
println!(" Every outcome now has the same number of patterns. The price is the");
println!(" patterns thrown away — 6 of 16 for `1..=10` — which is why the real");
println!(" implementation draws again rather than returning `None`.\n");
println!("5. What that costs in practice");
for n in [3u32, 6, 8, 10] {
let kept = (16 / n) * n;
println!(
" 1..={n:<3} keeps {kept:>2}/16 patterns -> {:.1}% of draws are redrawn",
(16 - kept) as f64 * 100.0 / 16.0
);
}
println!(" With a real 64-bit generator the discarded slice is vanishingly small,");
println!(" so the correct version is effectively free. The 4-bit generator here is");
println!(" small on purpose: it makes the whole distribution countable.");
}
Verified output of randomness_kata.rs — regenerated by tools/run_examples.py, never hand-typed.
1. Same seed, same sequence — asserted, not eyeballed
A: [7, 4, 15, 3, 2]
B: [7, 4, 15, 3, 2]
assert_eq!(A, B) passed — reproducibility is a property you can TEST,
which is the reason a library exposes seeding at all.
2. A range that divides the output space, and one that does not
The generator emits 4 bits: 16 equally likely patterns.
1..=8 (16 % 8 == 0) 1:2 2:2 3:2 4:2 5:2 6:2 7:2 8:2 (16/16 patterns used, spread 2..2)
1..=10 (16 % 10 == 6) 1:2 2:2 3:2 4:2 5:2 6:2 7:1 8:1 9:1 10:1 (16/16 patterns used, spread 1..2)
`1..=8` is already fair: every outcome gets exactly 2 patterns.
`1..=10` is not, and the six low outcomes are twice as likely.
3. The bias is a property of the ARITHMETIC, not of the generator
1..=3 (16 % 3 == 1) 1:6 2:5 3:5 (16/16 patterns used, spread 5..6)
1..=6 (16 % 6 == 4) 1:3 2:3 3:3 4:3 5:2 6:2 (16/16 patterns used, spread 2..3)
No seed appears in section 2 or 3. Every pattern is counted once,
so a better generator cannot fix this — only better arithmetic can.
4. Reject and redraw, and the skew goes to zero
1..=10 rejecting 1:1 2:1 3:1 4:1 5:1 6:1 7:1 8:1 9:1 10:1 (10/16 patterns used, spread 1..1)
1..=3 rejecting 1:5 2:5 3:5 (15/16 patterns used, spread 5..5)
1..=6 rejecting 1:2 2:2 3:2 4:2 5:2 6:2 (12/16 patterns used, spread 2..2)
Every outcome now has the same number of patterns. The price is the
patterns thrown away — 6 of 16 for `1..=10` — which is why the real
implementation draws again rather than returning `None`.
5. What that costs in practice
1..=3 keeps 15/16 patterns -> 6.2% of draws are redrawn
1..=6 keeps 12/16 patterns -> 25.0% of draws are redrawn
1..=8 keeps 16/16 patterns -> 0.0% of draws are redrawn
1..=10 keeps 10/16 patterns -> 37.5% of draws are redrawn
With a real 64-bit generator the discarded slice is vanishingly small,
so the correct version is effectively free. The 4-bit generator here is
small on purpose: it makes the whole distribution countable.
See also¶
- A throwaway that needs a crate — the error before the ones on this page:
rand::…in a loose.rsfile, and the three commands that give it a project to live in - Adding a dependency — why
rand = "0.10"is a range, and how to see what resolved expect— the.expect("Failed to read line")on the guessing game's second half- What a struct is — the generator above is one, with its state as the only field
Po polsku¶
W bibliotece standardowej Rusta nie ma żadnego generatora liczb pseudolosowych — i to jest decyzja, a nie luka. Losowość to pytanie o politykę przebrane za API: kryptograficzny czy szybki, zasiewany ziarnem (seed) dla powtarzalności czy pobierany z systemu operacyjnego, jaki rozkład. std jest stabilne na zawsze, więc nie może później zmienić zdania; dlatego zostawia to crate'owi rand, który swoje odpowiedzi może wersjonować. W std zostają tylko RandomState i DefaultHasher — zasiewane przez system, ale to narzędzia do haszowania, a nie generator.
Tu zaczyna się pułapka, która polskiego czytelnika dotyka mocniej niż angielskiego: każdy przekład i każdy polski kurs jest z definicji starszy od oryginału, z którego powstał, a rand przemianował swoje API już dwa razy. Klasyczna gra w zgadywanie liczby z drugiego rozdziału Rust Booka, przepisana w dowolnym polskim poradniku, nie zbuduje się z żadnym rand, który zainstalujesz dzisiaj — i to w najgorszym możliwym porządku, bo błędy przychodzą po kolei. Najpierw error[E0425]: cannot find function thread_rng in crate rand (dziś jest rand::rng()), a po poprawieniu tej linijki error[E0599]: no method named random_range found for struct ThreadRng — bo metoda przeniosła się na cechę (trait) RngExt, więc use rand::Rng; kompiluje się bez słowa skargi i dopiero wywołanie nie działa. Jeśli losujesz raz, cała odpowiedź to wolna funkcja rand::random_range(1..=100), bez żadnego use. I warto pamiętać, że rand = "0.10" to zakres, a nie wersja: cargo tree | head -3 pokazuje, co naprawdę się rozwiązało, i to pierwsza rzecz do sprawdzenia, gdy kod z kursu nie chce się zbudować.
Druga rzecz warta zapamiętania to znaczenie samego słowa. „Losowy” w programie znaczy nieprzewidywalny, jeśli nie znasz ziarna — a nie niepowtarzalny. Dwa generatory z tym samym ziarnem generują tę samą sekwencję na zawsze i to jest ta użyteczna połowa, bo dopiero ona pozwala testować program, który losuje (w tej bibliotece wszystkie przykłady mają własny zasiewany generator właśnie dlatego — wynik musi się zgadzać z zapisanym kluczem odpowiedzi co do znaku). I nie zwijaj losowania do zakresu samodzielnie przez 1 + x % n: strona wylicza to dokładnie na czterech bitach, gdzie 16 równie prawdopodobnych wzorców rozłożonych na 1..=10 daje sześć wyników dwa razy częstszych od pozostałych czterech. To obciążenie (modulo bias) jest własnością arytmetyki, nie generatora, więc lepsze źródło bitów go nie naprawi — rand odrzuca i dolosowuje te wartości, które przechyliłyby wynik.
Szukaj po polsku: generator liczb pseudolosowych w Ruscie · ziarno generatora · obciążenie modulo · rust rand thread_rng not found · rand RngExt random_range · rust modulo bias rejection sampling