#Random
Status: standard library specification draft.
This chapter defines std.random, which lib/std writes in ordinary hd
over the language tier:
- the host capability trait
Random; SeededRandom, its deterministic provider;Rng, a seeded generator with the draws that scripts need, andrng.
Which provider a command binds is CLI tier (Host Capabilities).
#Random Source
Random draws values from a source of randomness:
pub trait Random:
fn next_u64(&mut self) -> u64
fn fill(&mut self, count: usize) -> List[u8]- std-random.decl-ref
std.randomdeclares the host capability traitRandomwith the methods above. Code imports it, as inuse std.random.Random. - std-random.next
next_u64returns the next value of the provider's source. - std-random.fill
fill(count)returnscountvalues of that source, as bytes. - std-random.plainBoth methods are plain calls, not bang calls.
- std-random.mut-ref.reference-valuesBoth take
&mut self, soRandomis a mutable requirement trait,$.use(Random)is a&mut dyn Randombyreq.ref.use.reference-values, and a seeded provider may advance its own state. - std-random.fill-free
std.randomdeclarespub fn fill(count: usize) -> List[u8] $ Random. It returnsfill(count)of theRandomprovider that covers the call.
use std.random.Random
fn roll() -> u64 $ Random:
$.use(Random).next_u64() % 6 + 1Why. A random draw returns a value from the host, as a clock read does, so it needs no driver. A test installs a seeded provider, and replay records each draw at the boundary.
Note.
Random.filland the freefillare the calls for a secret, such as a session token (hex_encode(fill(16))), because the default host provider draws from the system source. AnRngrepeats for one seed, so it is never a source of secrets.
Note. A property test's random draws also come from a
Randomprovider, one thatstd.testingseeds from the test runner (std-testing.runner.random).
#Seeded Random
SeededRandom is the deterministic Random provider. A test supplies it
with $.with, and each seed gives one fixed sequence of draws:
use std.random.{Random, SeededRandom}
fn roll() -> u64 $ Random:
$.use(Random).next_u64() % 6 + 1
tests:
use std.testing.assert_equal
it("one seed, one sequence"):
first := SeededRandom::new(7)
again := SeededRandom::new(7)
let rolled: u64 = 0
$.with(Random=first):
rolled = roll()
$.with(Random=again):
assert_equal(roll(), rolled, reason="the same seed rolls the same")- std-random.seeded.decl-ref
std.randomdeclares the structSeededRandom, which implementsRandom, with private fields. Code imports it, as inuse std.random.SeededRandom. Its methods take&mut self, as the trait's do, so aSeededRandomserves as a provider through a reference to it. - std-random.seeded.new-ref
SeededRandom::new(seed: u64) -> *mut SeededRandomreturns a provider thatseedstarts, a new object. A provider installed from that reference, and every copy of it, draw from the one state, so a draw through the provider advances the generator the caller holds. - std-random.seeded.same-seedTwo providers that one seed starts return the same values for the same sequence of
next_u64andfillcalls. - std-random.seeded.no-hostA
SeededRandomdraws from its own state only. It reads nothing from the host.
Note. These rules fix no generator, so a seed's values may differ between implementations. A test compares draws with each other, not with written numbers. An
Rngfixes its generator, as Rng Generator states.
See also: Mutable Providers.
#Rng
Rng is a generator value with the draws that scripts need. A seed starts
it, or rng seeds it from the Random provider:
use std.random.{Random, Rng, rng}
fn roll() -> i64 $ Random:
dice := rng()
dice.int(1..=6)
fn deal(seed: u64) -> List[string]:
deck := Rng::new(seed)
let mut cards: List[string] = ["A", "K", "Q", "J"]
deck.shuffle(&mut cards)
cards| Rule | Method | Returns |
|---|---|---|
std-random.rng.next-refnext_u64 |
pub fn next_u64(&mut self) -> u64 |
the next 64-bit draw |
std-random.rng.int.generic-refint |
pub fn int[N < Integer](&mut self, range: Range[N]) -> N |
an integer of range, each with equal probability |
std-random.rng.float-reffloat |
pub fn float(&mut self) -> f64 |
an f64 from 0.0 up to, not including, 1.0 |
std-random.rng.bool-refbool |
pub fn bool(&mut self) -> bool |
true or false, each with equal probability |
std-random.rng.choose-refchoose |
pub fn choose[T](&mut self, items: List[T]) -> T? |
one item, each position with equal probability |
std-random.rng.shuffle-refshuffle |
pub fn shuffle[T](&mut self, items: &mut List[T]) -> void |
nothing: it reorders items, each order with equal probability |
std-random.rng.sample-refsample |
pub fn sample[T](&mut self, items: List[T], count: usize) -> List[T] |
count items from distinct positions, in random order |
- std-random.rng.decl-struct
std.randomdeclares the structRng, with private fields, and the methods above. Code imports it, as inuse std.random.Rng. - std-random.rng.new-ref
Rng::new(seed: u64) -> *mut Rngreturns a generator thatseedstarts, a new object, asSeededRandom::newreturns a provider. Every copy of the reference, and every callee given it, draws from that one generator. - std-random.rng.from-random-ref
std.randomdeclarespub fn rng() -> *mut Rng $ Random. It draws onenext_u64from theRandomprovider that covers the call, and returnsRng::newof that draw. - std-random.rng.plainEvery
Rngmethod is a plain call with the empty requirement row. AnRngreads nothing from the host. - std-random.rng.ref-receiversEvery method takes
&mut self, since each draw advances the generator.
Why. A script that draws many values pays for one host draw, and a seeded
Rngneeds no provider at all. Go'smath/rand/v2pairs a seeded generator value with a host-seeded default the same way.
#Rng Generator
An Rng's generator is fixed, so a seed gives the same draws in every
implementation:
| Call | Draws |
|---|---|
Rng::new(42), next_u64 three times |
12186209167316039401, 2060537544740691614, 1459241681827062570 |
Rng::new(0), next_u64 twice |
16359567918072254692, 230122937566968419 |
Rng::new(7), int(1..=6) ten times |
6, 5, 2, 5, 5, 5, 4, 4, 1, 6 |
Rng::new(7), float |
0.23382771772151634 |
- std-random.rng.generatorAn
Rng's state is four 32-bit words, and each 32-bit draw is one step of xoshiro128** 1.0, as the reference code below gives. - std-random.rng.seeding
Rng::newfolds the seed to the 32-bitz = (seed ^ (seed >> 32)) mod 2^32. Then for each state word in order,zadvances by0x9E3779B9modulo 2^32, and the word ismix32(z). - std-random.rng.mix
mix32is the 32-bit finalizer of MurmurHash3, as the reference code gives. - std-random.rng.next-u64
next_u64takes two 32-bit draws. The first is the high half of the result, and the second the low half.
uint32_t mix32(uint32_t z) {
z = (z ^ (z >> 16)) * 0x85EBCA6B;
z = (z ^ (z >> 13)) * 0xC2B2AE35;
return z ^ (z >> 16);
}
uint32_t rotl(uint32_t x, int k) { return (x << k) | (x >> (32 - k)); }
uint32_t next32(uint32_t s[4]) {
uint32_t result = rotl(s[1] * 5, 7) * 9;
uint32_t t = s[1] << 9;
s[2] ^= s[0]; s[3] ^= s[1]; s[1] ^= s[2]; s[0] ^= s[3];
s[2] ^= t;
s[3] = rotl(s[3], 11);
return result;
}
Why. A seeded
Rngserves output that must repeat, such as a level that a seed names or a fixture's sample. Its draws therefore cannot depend on the implementation. xoshiro128** needs only 32-bit steps, so no implementation needs a wide multiplication.
Note.
lib/std'sSeededRandomuses the same generator, but Seeded Random does not require it.
#Integer Ranges
int takes either form of Range, so 0..10 draws from 0 through 9 and
1..=6 from 1 through 6. It is generic over the integer type, so int(0..items.len()) is a usize and int(0..=255) can be a u8:
- std-random.rng.int.countThe count
nofrangeisend - startfor a..range andend - start + 1for a..=range, computed exactly. - std-random.rng.int.emptyA range with no integer, where
nis 0 or less, panics. Panic:explicit-panic. - std-random.rng.int.offset
intreturnsstart + below(n). - std-random.rng.int.u64
intdraws and counts inu64, converting bounds and the result withInteger.to_u64andInteger.from_u64. It never overflowsN, and a negative bound is converted through its magnitude. For ani64range, a seed draws the same values as thei64-onlyintdid. - std-random.rng.below
below(n)drawsnext_u64values until one,x, is at least2^64 mod n. It returnsx mod n. - std-random.rng.below.fullWhen
nis 2^64, the wholei64oru64range,below(n)is onenext_u64draw.
use std.random.Rng
fn two_rolls(seed: u64) -> List[i64]:
dice := Rng::new(seed)
[dice.int(0..10), dice.int(1..=6)]
# two_rolls(7) is [7, 5]Why. Rejection keeps the result unbiased. The accepted draws, from
2^64 mod nup, number a multiple ofn, so each remainder has the same share. Fewer than two draws are needed on average, as in OpenBSD'sarc4random_uniform.
Note.
belowis not a public method. The rules name it so that the list methods can use it.
#Floats And Booleans
- std-random.rng.float.bits
floatis the top 53 bits of onenext_u64draw, times 2^-53. So it is a multiple of 2^-53, and below 1.0. - std-random.rng.bool.bit
boolistrueexactly when the top bit of onenext_u64draw is set.
use std.random.Rng
fn coin_and_fraction(seed: u64) -> (f64, bool):
gen := Rng::new(seed)
(gen.float(), gen.bool())
# coin_and_fraction(7) is (0.23382771772151634, false)#Lists
- std-random.rng.choose.indexFor a list that is not empty,
choosereturns.Some(items[below(len)]). - std-random.rng.choose.emptyFor an empty list,
choosereturns.Noneand draws nothing. - std-random.rng.shuffle.order
shuffleis the Fisher–Yates shuffle from the back. For eachifromlen - 1down to 1, it swaps the items atiand atbelow(i + 1). - std-random.rng.shuffle.permutationSo
shufflekeeps every item and changes only their order. - std-random.rng.shuffle.shortA list of fewer than two items is unchanged, and
shuffledraws nothing. - std-random.rng.sample.order
sampleshuffles a copy ofitemsfrom the front, forcountsteps. Stepi, from 0, swaps the copy's items atiand ati + below(len - i). - std-random.rng.sample.result
samplereturns the copy's firstcountitems. - std-random.rng.sample.input
itemsitself is unchanged. - std-random.rng.sample.countA
countabove the list's length panics. Panic:explicit-panic.
Call on Rng::new(7) |
Result |
|---|---|
shuffle of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] |
[10, 3, 7, 4, 6, 5, 1, 9, 2, 8] |
sample([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 3) |
[8, 3, 4] |
choose(["a", "b", "c"]) |
.Some("c") |
use std.random.Rng
fn pick_three(seed: u64) -> List[i64]:
gen := Rng::new(seed)
gen.sample([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 3)
# pick_three(7) is [8, 3, 4]
fn one_letter(seed: u64) -> string?:
gen := Rng::new(seed)
gen.choose(["a", "b", "c"])
# one_letter(7) is .Some("c")Why. A count past the length is a bug in the caller, so it panics, as
chunks(0)does. Python'srandom.samplerejects it too.
See also: Range Expressions.