Utilities · Lesson 20.5

Random

Source
Build a Pcg64Dxsm generator from a fixed seed and see that the same seed always replays the same numbers.

Arithmetic alone cannot produce randomness. What a random number generator produces is a sequence that looks random but follows entirely from where it started — its seed. Give it the same seed and you get the same numbers, in the same order, on every machine and every run.

That sounds like a weakness, and it is the opposite. A simulation can be rerun exactly, a test that draws random data can be repeated, and a bug that only showed up for one particular draw can be reproduced — all by remembering one number.

The program imports the Random package's general-purpose generator and one way of drawing from it:

import Random::{ Pcg64Dxsm, UniformBelow };

A generator is a struct

Pcg64Dxsm(seed) builds a generator. It is an ordinary struct whose fields are its state, and every draw advances that state. UniformBelow<Pcg64Dxsm>(generator, bound) draws a number from 0 up to, but not including, bound — so 100 gives 0 to 99:

func Draw(label: char8[..], generator: &var Pcg64Dxsm) {
    Print("{}", label);
    for i in 0..6 {
        Print(" {:2}", UniformBelow<Pcg64Dxsm>(generator, 100));
    }
    PrintLine();
}

Because a draw changes the generator, Draw takes it as a mutable reference, &var Pcg64Dxsm: the six draws advance the caller's generator, not a copy of it. The {:2} pads each number to two columns, as in Format.

UniformBelow is generic over any generator, which is why the call names the type in angle brackets. The compiler can also work it out from the argument; the program spells it out so you can see which generator is drawing.

Same seed, same numbers

var first = Pcg64Dxsm(2026);
Draw("seed 2026      ", first);

var second = Pcg64Dxsm(2026);
Draw("seed 2026 again", second);

var other = Pcg64Dxsm(2027);
Draw("seed 2027      ", other);

The first two lines of output are identical: 23 73 75 87 37 28, twice. The third has nothing in common with them, even though 2027 is right next to 2026 — a seed is a label for a sequence, not a starting value that nearby seeds start close to.

Drawing continues

Calling Draw on first again does not start over. It carries on from where the first six draws left the state, and gives six new numbers.

A copy replays the future

Copying a generator copies its state, as copying any struct copies its fields. The copy then produces exactly what the original is about to produce:

var copy = first;
Draw("2026 continued ", first);
Draw("its copy       ", copy);
flowchart LR
    seed(["seed 2026"]) --> s0["state after<br/>12 draws"]
    s0 -- "var copy = first" --> c["copy:<br/>same state"]
    s0 -- "Draw(first)" --> f["16 12 33 48 2 29"]
    c -- "Draw(copy)" --> g["16 12 33 48 2 29"]

That is useful on purpose — saving a generator and rewinding to it — and a trap by accident, when two parts of a program each hold a copy and draw "different" numbers that are the same.

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// Arithmetic alone cannot produce randomness. What a random number generator produces is a
// sequence that *looks* random but follows entirely from where it started, its seed. Give it
// the same seed and you get the same numbers, in the same order, on every machine, every run.
//
// That is a feature. A simulation can be rerun exactly, a test that draws random data can be
// repeated, and a bug that depended on the draw can be reproduced, all by remembering one number.
//
// The Random package's general-purpose generator is `Pcg64Dxsm`. `Pcg64Dxsm(seed)` builds one,
// and `UniformBelow<Pcg64Dxsm>(generator, bound)` draws a number from 0 up to, not including,
// `bound`. The generator is an ordinary struct whose fields are its state: each draw advances
// it, which is why it is passed as `&var`. And like any struct, copying it copies the state,
// so a copy replays exactly what the original is about to produce.
import Io::{ Print, PrintLine };
import Random::{ Pcg64Dxsm, UniformBelow };

func Draw(label: char8[..], generator: &var Pcg64Dxsm) {
    Print("{}", label);
    for i in 0..6 {
        Print(" {:2}", UniformBelow<Pcg64Dxsm>(generator, 100));
    }
    PrintLine();
}

func Main() -> int {
    var first = Pcg64Dxsm(2026);
    Draw("seed 2026      ", first);

    // A second generator from the same seed: the same run, exactly.
    var second = Pcg64Dxsm(2026);
    Draw("seed 2026 again", second);

    // A neighbouring seed gives an unrelated run. Seeds are labels, not starting values.
    var other = Pcg64Dxsm(2027);
    Draw("seed 2027      ", other);

    // Drawing again continues where the generator stopped; it does not start over.
    Draw("2026 continued ", first);

    // A copy holds the same state, so it produces the same next numbers as the original.
    var copy = first;
    Draw("2026 continued ", first);
    Draw("its copy       ", copy);
    return 0;
}

Besides Io, its Rux.toml lists Random under [Dependencies].

Run it

cd Examples/Utilities/Random
rux run
seed 2026       23 73 75 87 37 28
seed 2026 again 23 73 75 87 37 28
seed 2027       73  1 34  5  0 38
2026 continued  67 36 72 16 94 29
2026 continued  16 12 33 48  2 29
its copy        16 12 33 48  2 29

Common mistakes

A generator declared with let.
Every draw changes the generator, so it must be mutable. With let first = Pcg64Dxsm(2026); the call Draw("seed 2026 ", first) fails with error: argument 2 to 'Draw' cannot borrow immutable 'first' as '&var Pcg64Dxsm', and the help says declare 'first' with 'var' to make it mutable.
Taking the generator by value.
Declare the parameter as generator: Pcg64Dxsm and the draw inside fails with error: argument 1 to 'UniformBelow' cannot borrow immutable 'generator' as '&var Pcg64Dxsm'. Even if the function copied it into a var local, every call would draw from a copy, and the caller's generator would never move on. Pass it as &var.
Seeding again for every draw.
A function that builds Pcg64Dxsm(2026) each time it is called returns the same numbers on every call. Make one generator, seed it once, and pass it to everything that draws.

Try it yourself

  1. Change the seed to your birth year. Do the first two lines still match each other?
  2. Pcg64Dxsm::WithStream(2026, 1) builds a generator with the same seed on a different stream. Draw from it and compare with seed 2026.
  3. Import Xoshiro256, a second generator, and draw from Xoshiro256(2026) with UniformBelow<Xoshiro256>. Same seed, different generator — same numbers?
  4. Import Bernoulli and flip ten coins with Bernoulli<Pcg64Dxsm>(generator, 0.5), which returns a bool.

Learn more

  • Distribution — shaping raw draws into dice, shuffles and bell curves
  • Entropy — seeds that nobody can guess
  • Mutable reference and Copy — the two ideas that explain how a generator behaves
  • Generic — why UniformBelow works with any generator