A process-overridable, seedable pseudo-random number generator.
In production the wrapper falls through to Erlang's :rand. In tests,
seed/1 pins a deterministic state in the calling process; subsequent calls
walk the same byte-precise stream every run – so a flake-prone path (jittered
backoff, a shuffled queue, an A/B tiebreaker) can be asserted exactly instead
of retried.
Inheritance
The seed lives in Ambient.ProcessOverride, so any child process spawned by
the test inherits it through:
- the implicit
$callerschain (Task.async/1,Agent.start/1– no setup needed), and - explicit
Ambient.Random.allow/1for long-lived processes.
Note: each process draws from a fork of the seeded stream, not one shared advancing stream. A child inherits the owner's state as of the seed and advances its own private copy, so the owner and a freshly-spawned child (or two sibling tasks) that each draw will produce the same sequence. This keeps results deterministic per process; it does not interleave draws across processes into one global sequence.
Production
Ambient.Random.uniform(100) # 1..100
Ambient.Random.shuffle(cards)
Ambient.Random.take_random(deck, 5)Tests
Ambient.Random.seed(42)
x = Ambient.Random.uniform(100) # same value every run
Ambient.Random.reset()Cryptography
Seedable and unpredictable are opposites: a stream reproducible from a
64-bit integer cannot be credential-grade, and :exsss leaks its internal
state to anyone who sees a handful of outputs. The split is therefore
per-function:
bytes/1– credential-safe in production. With no seed in scope it is:crypto.strong_rand_bytes/1, and production builds don't compile the override machinery in (Ambient.ProcessOverride.enabled?/0), so no ambient context can downgrade it. Underseed/1, in a test build, it is deterministic and not safe.uniform/0,1,normal/2,shuffle/1,random/1,take_random/2–:rand-backed, never credential-grade in any build. Don't hand-roll a token out ofuniform(256).
Ambient.Credo.NoDirectRandom keeps direct :rand / Enum.shuffle calls
from creeping back in.
Summary
Functions
Authorise child_pid to read owner_pid's overrides. For long-lived
processes that don't appear in the $callers chain.
Return n random bytes.
Drop every override this process owns in this module's table.
Drop this process's override for key. No-op if there isn't one.
Random float from a normal distribution. Mirrors :rand.normal_s/3, so the
second argument is the variance (σ²), not the standard deviation –
normal(0, 9) has σ = 3.
Whether an override for key is in scope for the calling process.
Set a process-local override for key. Auto-cleaned when the process exits.
Pick one element at random. Seed-respecting drop-in for Enum.random/1.
Drop the per-process seed; subsequent calls use real randomness.
Pin the per-process RNG to a deterministic stream derived from seed.
Inherited by child processes via the $callers chain.
Whether a seed is currently in effect for this process.
Return this module's table to private, process-scoped mode.
Make owner_pid's overrides the ones every process reads. async: false
only – see Ambient.ProcessOverride.set_shared/2.
Shuffle an enumerable. Seed-respecting drop-in for Enum.shuffle/1.
Take n random elements without replacement. Seed-respecting drop-in for
Enum.take_random/2.
Random float in [0.0, 1.0). Mirrors :rand.uniform_s/1.
Random integer in 1..n. Mirrors :rand.uniform_s/2. Raises when n < 1.
Functions
Authorise child_pid to read owner_pid's overrides. For long-lived
processes that don't appear in the $callers chain.
@spec bytes(non_neg_integer()) :: binary()
Return n random bytes.
With no seed in scope this is :crypto.strong_rand_bytes/1 – so it is safe
for credential material in production, where the override machinery isn't
compiled in at all (see Ambient.ProcessOverride.enabled?/0) and this is the
only reachable clause.
Under seed/1 it instead draws from the deterministic, non-cryptographic
stream, so a test can assert an exact token. That path exists only in builds
that opted into overrides.
Unlike the other functions here, bytes/1 therefore does not fall through
to :rand.
@spec delete_all() :: :ok
Drop every override this process owns in this module's table.
@spec delete_override(term()) :: :ok
Drop this process's override for key. No-op if there isn't one.
Random float from a normal distribution. Mirrors :rand.normal_s/3, so the
second argument is the variance (σ²), not the standard deviation –
normal(0, 9) has σ = 3.
Whether an override for key is in scope for the calling process.
Set a process-local override for key. Auto-cleaned when the process exits.
@spec random(Enumerable.t()) :: any()
Pick one element at random. Seed-respecting drop-in for Enum.random/1.
@spec reset() :: :ok
Drop the per-process seed; subsequent calls use real randomness.
@spec seed(integer()) :: :ok
Pin the per-process RNG to a deterministic stream derived from seed.
Inherited by child processes via the $callers chain.
@spec seeded?() :: boolean()
Whether a seed is currently in effect for this process.
@spec set_private() :: :ok
Return this module's table to private, process-scoped mode.
@spec shuffle(Enumerable.t()) :: list()
Shuffle an enumerable. Seed-respecting drop-in for Enum.shuffle/1.
@spec take_random(Enumerable.t(), non_neg_integer()) :: list()
Take n random elements without replacement. Seed-respecting drop-in for
Enum.take_random/2.
@spec uniform() :: float()
Random float in [0.0, 1.0). Mirrors :rand.uniform_s/1.
@spec uniform(pos_integer()) :: pos_integer()
Random integer in 1..n. Mirrors :rand.uniform_s/2. Raises when n < 1.