Ambient.Random (Ambient v0.1.0)

Copy Markdown View Source

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:

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:

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

allow(child_pid, owner_pid \\ self())

@spec allow(pid(), pid()) :: :ok

Authorise child_pid to read owner_pid's overrides. For long-lived processes that don't appear in the $callers chain.

bytes(n)

@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.

delete_all()

@spec delete_all() :: :ok

Drop every override this process owns in this module's table.

delete_override(key)

@spec delete_override(term()) :: :ok

Drop this process's override for key. No-op if there isn't one.

normal(mean, variance)

@spec normal(number(), number()) :: float()

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.

overridden?(key)

@spec overridden?(term()) :: boolean()

Whether an override for key is in scope for the calling process.

put_override(key, value)

@spec put_override(term(), term()) :: :ok

Set a process-local override for key. Auto-cleaned when the process exits.

random(enum)

@spec random(Enumerable.t()) :: any()

Pick one element at random. Seed-respecting drop-in for Enum.random/1.

reset()

@spec reset() :: :ok

Drop the per-process seed; subsequent calls use real randomness.

seed(seed)

@spec seed(integer()) :: :ok

Pin the per-process RNG to a deterministic stream derived from seed. Inherited by child processes via the $callers chain.

seeded?()

@spec seeded?() :: boolean()

Whether a seed is currently in effect for this process.

set_private()

@spec set_private() :: :ok

Return this module's table to private, process-scoped mode.

set_shared(owner_pid \\ self())

@spec set_shared(pid()) :: :ok

Make owner_pid's overrides the ones every process reads. async: false only – see Ambient.ProcessOverride.set_shared/2.

shuffle(enum)

@spec shuffle(Enumerable.t()) :: list()

Shuffle an enumerable. Seed-respecting drop-in for Enum.shuffle/1.

take_random(enum, n)

@spec take_random(Enumerable.t(), non_neg_integer()) :: list()

Take n random elements without replacement. Seed-respecting drop-in for Enum.take_random/2.

uniform()

@spec uniform() :: float()

Random float in [0.0, 1.0). Mirrors :rand.uniform_s/1.

uniform(n)

@spec uniform(pos_integer()) :: pos_integer()

Random integer in 1..n. Mirrors :rand.uniform_s/2. Raises when n < 1.