Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Draw random values and make secrets

Plenty has two sources of random values, and they are not interchangeable. std.random gives repeatable sequences for simulations, games, and tests. std.secrets gives unpredictable bytes from the operating system for tokens and keys.

For record and request identifiers, see std.uuid, which builds standard UUIDs from OS entropy.

Seeded generators

A generator is a value you create from a seed and own. Nothing is seeded for you and there is no shared generator behind the scenes, so the same seed always produces the same run:

import std.random

def main() -> Result[(), AllocError | IoError | std.random.RandomError]:
    mut rng = std.random.seeded(1234567u64)
    print(rng.next_u64())?
    print(rng.between(1, 7)?, rng.between(1, 7)?, rng.between(1, 7)?)?
    print(rng.next_bool(), rng.next_f64() < 1.0)?
    mut cards = ["ace", "king", "queen", "jack", "ten"]?
    rng.shuffle(&mut cards)
    print(cards)?
    print(rng.sample_indices(10, 3)?)?
    print(cards[rng.index(len(cards))?])?
    Ok(())
3504822795582309479
5 2 1
True True
["ace", "king", "ten", "queen", "jack"]
[3, 1, 9]
king

Drawing changes the generator, so it must be a mut binding. Ranges are half-open: between(1, 7) is a die roll from 1 to 6. Every bounded draw is unbiased. shuffle reorders a list in place without copying its elements. sample_indices(10, 3) picks three different positions out of ten; index your collection with them. index(count) picks one.

Because the sequence depends only on the seed, a failing simulation can be replayed exactly. state() and from_state checkpoint a generator part-way through:

import std.random

def main() -> Result[(), AllocError | IoError | std.random.RandomError]:
    mut first = std.random.seeded(42u64)
    mut second = std.random.seeded(42u64)
    print(first.next_u64() == second.next_u64())?
    (a, b, c, d) = first.state()
    mut resumed = std.random.from_state(a, b, c, d)?
    print(first.between(0, 100)? == resumed.between(0, 100)?)?
    print(str.repr(first.between(5, 5))?)?
    print(str.repr(first.below(0u64))?)?
    Ok(())
True
True
Result[i64, std.random.RandomError].Err(std.random.RandomError.EmptyRange)
Result[u64, std.random.RandomError].Err(std.random.RandomError.EmptyRange)

An empty range is an error, never a clamped or default value, because it can come from data such as an empty list. A rejected request does not advance the generator. An argument no call could accept, such as sample_indices(3, 4) asking for four of three positions, is a bug in the caller: it stops the program with a trap report.

The sequence for a seed is part of the language’s contract and does not change between Plenty versions. It is also predictable by anyone who sees a few outputs, so never use a seeded generator for a password, token, or key.

Secrets

std.secrets asks the operating system for every byte. Its results differ on every run, so this program prints sizes rather than values:

import std.bytes
import std.secrets

def main() -> Result[(), AllocError | IoError | std.bytes.BytesError | std.secrets.SecretsError]:
    token = std.secrets.token_urlsafe(32)?
    print(len(token))?
    print(len(std.secrets.token_hex(16)?))?
    key = std.secrets.token_bytes(32)?
    print(key.len())?
    print(token != std.secrets.token_urlsafe(32)?)?
    print(std.secrets.below(6u64)? < 6u64)?
    mut nonce = std.bytes.empty()?
    nonce.resize(12, 0u8)?
    std.secrets.fill(&mut nonce, 0, 12)?
    print(nonce.len())?
    print(str.repr(std.secrets.token_hex(-1))?)?
    Ok(())
43
32
32
True
True
12
Result[str, std.secrets.SecretsError].Err(std.secrets.SecretsError.Allocation(AllocError.CapacityOverflow))

The argument is a number of secret bytes, not characters: 32 bytes become 43 base64url characters or 64 hex digits. token_urlsafe is safe to put in a URL or a file name. below is an unbiased integer, for example to pick a winner.

A negative count is a size that cannot be allocated, so it returns Allocation(AllocError.CapacityOverflow).

If the system cannot supply entropy, these functions return SecretsError.Entropy. They never fall back to a weaker source, and a failed fill leaves zeros, not a partly random buffer.

To start a simulation from an unpredictable seed, pass std.secrets.random_u64()? to std.random.seeded and print the seed so the run can be repeated.

For normal, exponential or floating uniform samples, pass that generator to a std.stat distribution. Each distribution documents its draw count, and failed sampling leaves the generator unchanged.

See the random reference for the algorithm and its stability contract, and the secrets reference for the failure policy and costs.