Rng
Pseudorandom values: integers, floats, choices, and shuffles.
Opt-in: nothing here is in scope until using Random, which brings both the seeded generator and the ambient conveniences into scope at once.
using Random
main do
IO.printLine(Random.between(1, 6))
IO.printLine(Random.shuffle(["a", "b", "c"]))
end
Two layers, for two needs. Rng is a small deterministic generator with explicit state: the same seed answers the same sequence on every run and on both backends, which is what makes randomized code testable. Random is the ambient convenience layer over it: each call draws fresh entropy from the host, so answers differ between runs.
let rng = Rng.seeded(42)
let (roll, next) = rng.nextBounded(6) # 0..5, then keep going with next
Random.integer(6) # 0..5, fresh entropy, foul
The generator is SplitMix64: tiny, fast, and good enough for modelling, games, sampling, shuffling, and randomized tests. It is NOT cryptographic: its state follows directly from its output, so never use it for secrets, tokens, or anything an adversary gets to see. The ambient layer draws its seeds from the host's secure source, but one secure seed does not make the stream that follows secure.
record Rng
The state of a deterministic generator: one 64-bit word.
A value, not a handle: every step answers a new Rng alongside its output, and the old one keeps answering what it always did. Thread the answer forward and the sequence is reproducible from its seed.
let rng = Rng.seeded(42)
let (a, rng) = rng.nextUint64
let (b, rng) = rng.nextUint64 # same `a` and `b` on every run
Build one with Rng.seeded rather than by hand: the record literal does no range reduction, and every step here assumes a state inside 0..2^64 - 1.
Fields
stateIntegeroptional
Methods
The draws on an Rng. Each answers its draw alongside the generator that follows it, so state threads through a chain of calls. All are methods, so Uniform Function Call Syntax reaches every one.
nextUint64
nextUint64 : (Integer, Rng)
Draws one 64-bit unsigned word and the generator that follows it.
This is the primitive everything else here is built on: the raw SplitMix64 output, uniformly spread over 0..2^64 - 1. Pure arithmetic over unbounded integers with an explicit mask, so both backends agree bit for bit with the reference C.
Returns the word and the next generator
Examples
let (word, next) = Rng.seeded(0).nextUint64
word # => 16294208416658607535
nextBounded
nextBounded(bound: Integer) -> (Integer, Rng)
Draws a uniform integer in 0..bound-1 and the generator that follows.
Uses rejection sampling rather than a bare remainder, so small bounds are not biased toward small answers: draws that would tilt the range are discarded and redrawn. Dies when bound is not positive or does not fit in one 64-bit word.
Parameters
bound- the exclusive upper bound, 1..2^64
Returns the draw and the next generator
Examples
let (roll, _) = Rng.seeded(42).nextBounded(6) # => 0..5
Rolling again with the threaded state
var rng = Rng.seeded(42)
let (first, advanced) = rng.nextBounded(6)
rng = advanced
nextFloat
nextFloat : (Float, Rng)
Draws a uniform float in 0.0..1.0 and the generator that follows.
Takes the top 53 bits of one word: exactly what a double's mantissa holds, so every representable value in range is reachable and 1.0 itself never comes out.
Returns the draw and the next generator
Examples
let (unit, _) = Rng.seeded(42).nextFloat # => 0.0..1.0
nextBoolean
nextBoolean : (Bool, Rng)
Draws a fair coin flip and the generator that follows.
Reads the top bit of one word rather than the bottom one, which is the bit a multiply-mixed generator decorrelates fastest.
Returns the flip and the next generator
Examples
let (heads, _) = Rng.seeded(42).nextBoolean
shuffle
shuffle(items: [A]) -> ([A], Rng)
Shuffles a list into a new order, answering the generator that follows.
A selection shuffle: each position draws uniformly among the elements not yet placed, so every permutation is equally likely. The input is untouched; shuffling an empty list answers an empty list.
Parameters
items- the elements to order
Returns the shuffled elements and the next generator
Examples
let (order, _) = Rng.seeded(42).shuffle([1, 2, 3])
sample
sample(items: [A], n: Integer) -> ([A], Rng)
Draws n distinct elements in random order, with the next generator.
Shuffles and takes the front: uniform over every ordered n-subset. Asking for more than the list holds answers the whole list shuffled; asking for none answers an empty list. Dies for a negative n.
Parameters
items- the elements to draw from
n- how many to draw, 0 or more
Returns the drawn elements and the next generator
Examples
let (hand, _) = Rng.seeded(42).sample([1, 2, 3, 4, 5], 2)
hand.count # => 2
choice
choice(items: [A]) -> (A?, Rng)
Draws one element uniformly, or None from an empty list.
The only fallible draw here, and the failure carries no information worth an error type: an empty list has no element to give, whatever the seed, so None is the whole story.
Parameters
items- the elements to draw from
Returns the drawn element, if any, and the next generator
Examples
let (pick, _) = Rng.seeded(42).choice(["a", "b", "c"])
["a", "b", "c"].contains?(pick.or("")) # => true
module Rng
Constructors and constants for the deterministic Rng. The draws themselves are methods in the make block below, so every one answers to Uniform Function Call Syntax: rng.nextBounded(6).
maxBound constant
One past the largest representable state: all arithmetic here is modulo 2^64, keeping the word closed under the mixing below. Also the largest exclusive bound one 64-bit draw can cover directly.
Public because the make block below lives outside this module and reaches it by qualification.
gamma constant
The SplitMix64 odd increment: every addition steps the state by a different odd multiple, so even a seed of 0 walks the whole space.
mixA constant
The two xor-shift/multiply mixing constants.
mixB constant
seeded
seeded(seed: Integer) -> Rng
Builds a generator from any integer seed.
The seed is reduced modulo 2^64, so negative seeds and huge ones are fine: every integer names a generator, and equal integers name the same one.
Parameters
seed- any integer; equal seeds answer equal sequences
Returns the generator
Examples
let (word, _) = Rng.seeded(42).nextUint64
word # => 13679457532755275413, always
Reproducible sampling in a test
let (pick, _) = Rng.seeded(7).choice(["a", "b", "c"])
module Random
Ambient randomness: fresh host entropy on every call.
Each function here draws its own seed from the host's secure source and runs the deterministic core above on it, so answers differ between runs the way the clock differs between reads. That is why every one is foul: the same call with the same arguments may answer differently.
Reach for Rng instead when the answers must repeat: seeded simulation, property tests, anything asserting on a particular draw.
fresh
fresh : Rng
Builds a generator from fresh host entropy.
The entry point for hand-threaded flows that still vary between runs: seed once here, then draw purely from the answer.
Returns a generator seeded from the host's secure source
Examples
var rng = Random.fresh()
let (roll, advanced) = rng.nextBounded(6)
rng = advanced
seeded
seeded(seed: Integer) -> Rng
Builds a generator from any integer seed. The deterministic entry: the same seed replays the same sequence, which is what tests want.
Parameters
seed- any integer; equal seeds answer equal sequences
Returns the generator
Examples
let (roll, _) = Random.seeded(42).nextBounded(6)
float
float : Float
Returns a uniform float in 0.0..1.0. Never answers 1.0 itself.
Returns the draw
Examples
Scaling into a range
let jitter = Random.float() * maxJitter
integer
integer(bound: Integer) -> Integer
Returns a uniform integer in 0..bound-1. Dies unless bound is positive and fits in 64 bits.
Parameters
bound- the exclusive upper bound, 1..2^64
Returns the draw
Examples
Random.integer(6) # => 0..5, like a die minus one
between
between(low: Integer, high: Integer) -> Integer
Returns a uniform integer in low..high, endpoints included. Dies unless low is below high and the span fits in 64 bits.
Parameters
low- the smallest answer
high- the largest answer
Returns the draw
Examples
Random.between(1, 6) # => a die roll
boolean
boolean : Bool
Returns a fair coin flip.
Returns the flip
Examples
if Random.boolean() then IO.printLine("heads") else IO.printLine("tails") end
chance?
chance?(p: Float) -> Bool
Returns true with probability p: a coin weighted by its argument. Dies unless p is inside 0.0..1.0.
Parameters
p- the chance of
true, 0.0..1.0
Returns true with probability p
Examples
Random.chance?(0.25) # => true about one call in four
Simulating a failure rate
if Random.chance?(0.01) then Error("flaky") else Ok(send(request)) end
choice
choice(items: [A]) -> A?
Returns a uniformly drawn element, or None from an empty list.
Parameters
items- the elements to draw from
Returns the drawn element, if any
Examples
Random.choice(["heads", "tails"]) # => Just("heads") or Just("tails")
sample
sample(items: [A], n: Integer) -> [A]
Returns n distinct elements in random order. Asking for more than the list holds answers the whole list shuffled. Dies for a negative n.
Parameters
items- the elements to draw from
n- how many to draw, 0 or more
Returns the drawn elements
Examples
Random.sample(["a", "b", "c", "d"], 2) # => two of the four
shuffle
shuffle(items: [A]) -> [A]
Returns the elements in a uniformly random order.
Parameters
items- the elements to order
Returns the shuffled elements
Examples
Random.shuffle([1, 2, 3, 4]) # => the four, in a random order