prelude.Rng
Part of prelude.
struct Rng
--- Seeded, reproducible random numbers (issue #20) ---
Float.random reads a process-global generator seeded from the clock, which is right for "give me something different each run" and useless for a test or a replay. An Rng is a VALUE: same seed, same sequence, on every platform and every run.
Every call returns the NEXT GENERATOR alongside the number, rather than mutating in place, so a generator is as ordinary a value as an Int. Threading it by hand is the cost; being able to replay a game from a seed, or write a test that cannot flake, is what it buys. A Ref[Rng] is the opt-in for in-place update.
--- Which generator, and why ---
SplitMix64: a small, modern 64-bit generator with a full 2^64 period. It is fast, has excellent bit diffusion, and is the right generator when reproducibility and simple state matter more than cryptographic secrecy. The wrapping multiply and logical shifts it needs became expressible when Int.wrapping_mul and bitwise operators landed.
Not for cryptography, keys, tokens or passwords. It is a statistical generator and its entire state is recoverable from two outputs.
let Rng.from_seed (seed: Int): Rng
Any Int is a legal seed, including zero and negatives. SplitMix64 has no forbidden state, unlike generators whose all-zero state gets stuck, so the seed is kept exactly rather than folded into a range.
let Rng.float (r: Rng): (Rng, Float)
A Float in [0.0, 1.0). Use 53 exactly representable bits so converting to Float cannot round the largest sample up to 1.0. The generator advances exactly once, as for previous versions; float samples may differ because the discarded low bits no longer round.
let Rng.float_range (r: Rng) (lo: Float) (hi: Float): (Rng, Float)
A float in [lo, hi), and the next generator.
let Rng.int_range (r: Rng) (lo: Int) (hi: Int): (Rng, Int)
[lo, hi), the upper bound EXCLUDED, so int_range(r, 0, xs.len()) is an index into xs and needs no - 1 anywhere. All ordered Int bounds are supported, even when hi - lo would overflow. hi must itself be an Int, so the maximum Int cannot be included as a result of this half-open API.
Rejection sampling, not z % span. The modulo is biased whenever the span does not divide the generator's range evenly: the low values come up more often, by about one part in range / span. That is invisible in a game and fatal in a shuffle, and the loop costs an extra step only on the fraction of draws that land in the leftover tail. Widths through the maximum Int preserve their seeded sequences. Wider ranges reject full signed samples outside [lo, hi) instead; they accept at least half of the domain, averaging at most two draws.
An empty or backwards range returns lo and does not draw, because there is no number to return and a panic here would make an ordinary off-by-one crash a program at random.
let Rng.bool (r: Rng): (Rng, Bool)
A boolean, evenly, and the next generator.
let Rng.shuffle (r: Rng) (xs: Array[T]): (Rng, Array[T])
Fisher-Yates, walking from the end so every permutation is equally likely. The naive version, swapping each element with any other, is not uniform, and looks fine until somebody counts.
let Rng.choice (r: Rng) (xs: Array[T]): (Rng, Option[T])
One element, or None from an empty array.