prng/random
This package provides many building blocks that can be used to define pure generators of pseudo-random values.
This is based on the great Elm implementation of Permuted Congruential Generators.
It is not cryptographically secure!
You can use this cheatsheet to navigate the module documentation:
Building generators | int, float, string, fixed_size_string, bit_array, uniform, weighted, choose, constant |
Transform and compose generators | map, then, pair |
Generating common data structures | fixed_size_list, list, fixed_size_dict, dict fixed_size_set, set |
Getting reproducible values out of generators | step, sample, to_iterator |
Getting truly random values out of generators | random_sample, to_random_iterator |
Types
A Generator(a)
is a data structure that describes how to produce random
values of type a
.
Take for example the following generator for random integers:
let dice_roll: Generator(Int) = random.int(1, 6)
It is just describing the values that can be generated - in this case the numbers from 1 to 6 - but it is not actually producing any value.
Getting values out of a generator
To actually get a value out of a generator you can use the step
function:
it takes a generator and a Seed
as input and produces a new seed and a
random value of the type described by the generator:
import prng/random
import prng/seed
let #(roll_result, updated_seed) = dice_roll |> random.step(seed.new(11))
roll_result
// -> 3
The generator is completely deterministic: this means that - given the same
seed - it will always produce the same results, no matter how many times you
call the step
function.
step
will produce an updated seed that you can use for subsequent calls to
get different pseudo-random results:
let initial_seed = seed.new(11)
let #(first_roll, new_seed) = dice_roll |> random.step(initial_seed)
let #(second_roll, _) = dice_roll |> random.step(new_seed)
#(first_roll, second_roll)
// -> #(3, 2)
pub opaque type Generator(a)
Constants
pub const max_int: Int = 2_147_483_647
The underlying algorith will work best for integers in the inclusive range
going from min_int
up to max_int
.
It can generate values outside of that range, but they are “not as random”.
Functions
pub fn choose(one: a, or other: a) -> Generator(a)
Generates two values with equal probability.
This is a shorthand for random.uniform(one, [other])
, but can read better
when there’s only two choices.
Examples
Given the following type to model the outcome of a coin flip:
pub type CoinFlip {
Heads
Tails
}
You can write a generator for coin flip outcomes like this:
let flip = random.choose(Heads, Tails)
pub fn constant(value: a) -> Generator(a)
Always generates the given value, no matter the seed used.
Examples
let always_eleven = random.constant(11)
random.random_sample(always_eleven)
// -> 11
pub fn dict(
keys keys: Generator(a),
values values: Generator(b),
) -> Generator(Map(a, b))
Generates a Map(k, v)
where each key value pair is generated using the
provided generators.
This is similar to fixed_size_dict
with the difference that the map is
going to have a random number of key-value pairs between 0 (inclusive) and
32 (inclusive).
pub fn fixed_size_dict(
keys keys: Generator(a),
values values: Generator(b),
of size: Int,
) -> Generator(Map(a, b))
Generates a Map(k, v)
where each key value pair is generated using the
provided generators.
⚠️ This function makes a best effort at generating a map with exactly the specified number of keys, but beware that it may contain less items if the keys generator cannot generate enough distinct keys.
pub fn fixed_size_list(
from generator: Generator(a),
of length: Int,
) -> Generator(List(a))
Generates a lists of a fixed size; its values are generated using the given generator.
Examples
Imagine you’re modelling a game of
Risk; when a player “attacks”
they can roll three dice. You may model that outcome using fixed_size_list
like this:
let dice_roll = random.int(1, 6)
let attack_outcome = random.fixed_size_list(dice_roll, 3)
pub fn fixed_size_set(
from generator: Generator(a),
of size: Int,
) -> Generator(Set(a))
Generates a Set(a)
where each item is generated using the provided
generator.
⚠️ This function makes a best effort at generating a set with exactly the specified number of items, but beware that it may contain less items if the given generator cannot generate enough distinct values.
pub fn fixed_size_string(size: Int) -> Generator(String)
Generates Strings with the given number number of UTF code points.
⚠️ The generated codepoints will be in the range from 0 (inclusive) to 1023 (inclusive). If you feel like these strings are not enough for your needs, please open an issue! I’d love to hear your use case and improve the package.
pub fn float(from: Float, to: Float) -> Generator(Float)
Generates floating point numbers in the given inclusive range.
Examples
let probability = random.float(0.0, 1.0)
pub fn int(from: Int, to: Int) -> Generator(Int)
Generates integers in the given inclusive range.
Examples
Say you want to model the outcome of a dice, you could use int
like this:
let dice_roll = random.int(1, 6)
pub fn list(generator: Generator(a)) -> Generator(List(a))
Generates a list with a random size with at most 32 items. Each item is generated using the given generator.
This is similar to fixed_size_list
with the difference that the size
is chosen randomly.
pub fn map(
generator: Generator(a),
with fun: fn(a) -> b,
) -> Generator(b)
Transforms the values produced by a generator using the given function.
Examples
Imagine you want to make a generator for boolean values that returns
True
and False
with the same probability. You could do that using map
like this:
let bool_generator = random.int(1, 2) |> random.map(fn(n) { n == 1 })
Here map
allows you to transform the values produced by the initial
integer generator - either 1 or 2 - into boolean values: when the original
generator produces a 1, bool_generator
will produce True
; when the
original generator produces a 2, bool_generator
will produce False
.
pub fn map2(
one: Generator(a),
other: Generator(b),
with fun: fn(a, b) -> c,
) -> Generator(c)
Combines two generators into a single one. The resulting generator produces
values obtained by applying fun
to the values generated by the given
generators.
## Examples
Imagine you need to generate random points in a 2D space:
pub type Point {
Point(x: Float, y: Float)
}
You can compose two basic generators into a Point
generator using map2
:
let x_generator = random.float(-1.0, 1.0)
let y_generator = random.float(-1.0, 1.0)
let point_generator = map2(x_generator, y_generator, Point)
Notice how you could get the same result using
then
:pub fn point_generator() -> Generator(Point) { use x <- random.then(random.float(-1.0, 1.0)) use y <- random.then(random.float(-1.0, 1.0)) random.constant(Point(x, y)) }
the
use
syntax paired withthen
may be confusing for other people reading your code, especially Gleam newcomers.Usually
map2
/map3
/… will be more than enough if you just need to combine simple generators into more complex ones.
pub fn map3(
one: Generator(a),
two: Generator(b),
three: Generator(c),
with fun: fn(a, b, c) -> d,
) -> Generator(d)
Combines three generators into a single one. The resulting generator
produces values obtained by applying fun
to the values generated by the
given generators.
Examples
Imagine you’re writing a generator for random enemies in a game you’re making:
pub type Enemy {
Enemy(health: Int, attack: Int, defense: Int)
}
Each enemy starts with a random health (that can go from 50 to 100) and
random values for the attack
and defense
stats (each can be in a range
from 1 to 5):
let health_generator = random.int(50, 100)
let attack_generator = random.int(1, 5)
let defense_generator = random.int(1, 5)
let enemy_generator =
random.map3(
health_generator,
attack_generator,
defense_generator,
Enemy,
)
pub fn map4(
one: Generator(a),
two: Generator(b),
three: Generator(c),
four: Generator(d),
with fun: fn(a, b, c, d) -> e,
) -> Generator(e)
Combines four generators into a single one. The resulting generator
produces values obtained by applying fun
to the values generated by the
given generators.
pub fn map5(
one: Generator(a),
two: Generator(b),
three: Generator(c),
four: Generator(d),
five: Generator(e),
with fun: fn(a, b, c, d, e) -> f,
) -> Generator(f)
Combines five generators into a single one. The resulting generator
produces values obtained by applying fun
to the values generated by the
given generators.
There’s no
map6
,map7
, and so on. If you feel like you need to compose together even more generators, you can use therandom.then
function.
pub fn pair(
one: Generator(a),
with other: Generator(b),
) -> Generator(#(a, b))
Generates pairs of values obtained by combining the values produced by the given generators.
Examples
let one_to_five = random.int(1, 5)
let probability = random.float(0.0, 1.0)
let ints_and_floats = random.pair(one_to_five, probability)
random.random_sample(ints_and_floats)
// -> #(3, 0.22)
pub fn random_sample(generator: Generator(a)) -> a
Generates a single value using the given generator.
The initial seed is chosen randomly so you won’t have control over which value is generated and may get different results each time you call this function.
This is useful if you want to quickly get a value out of a generator and
do not care about reproducibility (if you want to decide which seed is
used for the generation process you’ll have to use random.step
).
Examples
Imagine you want to perform some action, say only 40% of the times. Your code may look like this:
let probability = random.float(0.0, 1.0)
case random.random_sample(probability) <= 0.4 {
True -> perform_action()
False -> Nil // do nothing
}
pub fn sample(from generator: Generator(a), with seed: Seed) -> a
Generates a single value using the given generator and seed.
This is just a shorthand for the step
function that drops the new
seed. It can be useful if you just need to get a single value out of
a generator and need the result to be reproducible.
pub fn set(generator: Generator(a)) -> Generator(Set(a))
Generates a Set(a)
where each item is generated using the provided
generator.
This is similar to fixed_size_set
with the difference that the set is
going to have a random size between 0 (inclusive) and 32 (inclusive).
pub fn step(generator: Generator(a), seed: Seed) -> #(a, Seed)
Steps a Generator(a)
producing a random value of type a
using the given
seed as the source of randomness.
The stepping logic is completely deterministic. This means that, given a seed and a generator, you’ll always get the same result.
This is why this function also returns a new seed that can be used to make
subsequent calls to step
to get other random values.
Stepping a generator by hand can be quite cumbersome, so I recommend you
try to_iterator
,
to_random_iterator
, or sample
instead.
Examples
let initial_seed = seed.new(11)
let dice_roll = random.int(1, 6)
let #(first_roll, new_seed) = random.step(dice_roll, initial_seed)
let #(second_roll, _) = random.step(dice_roll, new_seed)
#(first_roll, second_roll)
// -> #(3, 2)
pub fn string() -> Generator(String)
Generates Strings with a random number of UTF code points, between 0 (included) and 32 (included).
This is similar to fixed_size_string
, with the difference that the
size is randomly generated as well.
pub fn then(
generator: Generator(a),
do generator_from: fn(a) -> Generator(b),
) -> Generator(b)
Transforms a generator into another one based on its generated values.
The random value generated by the given generator is fed into the do
function and the returned generator is used as the new generator.
Examples
then
is a really powerful function, almost all functions exposed by this
library could be defined in term of it!
Take as an example map
, it can be implemented like this:
fn map(generator: Generator(a), with fun: fn(a) -> b) -> Generator(b) {
random.then(generator, fn(value) {
random.constant(fun(value))
})
}
Notice how the do
function needs to return a Generator(b)
, you can
achieve that by wrapping any constant value with the random.constant
generator.
Code written with
then
can gain a lot in readability if you use theuse
syntax, especially if it has some deep nesting. As an example, this is how you can rewrite the previous example taking advantage ofuse
:fn map(generator: Generator(a), with fun: fn(a) -> b) -> Generator(b) { use value <- random.then(generator) random.constant(fun(value)) }
pub fn to_iterator(
generator: Generator(a),
seed: Seed,
) -> Iterator(a)
Turns the given generator into an infinite stream of random values generated with it.
seed
is the seed used to get the initial random value and start the
infinite sequence.
If you don’t care about the initial seed and reproducibility is not your
goal, you can use to_random_iterator
which works like this function and
randomly picks the initial seed.
pub fn to_random_iterator(
from generator: Generator(a),
) -> Iterator(a)
Turns the given generator into an infinite stream of random values generated with it.
The initial seed is chosen randomly so you won’t have control over which values are generated and may get different results each time you call this function.
If you want to have control over the initial seed used to get the infinite
sequence of values, you can use to_iterator
.
pub fn try_uniform(options: List(a)) -> Result(Generator(a), Nil)
This function works exactly like uniform
but will return an Error(Nil)
if the provided argument is an empty list since the generator wouldn’t be
able to produce any value in that case.
It generates values from the given list with equal probability.
Examples
random.try_uniform([])
// -> Error(Nil)
For example if you consider the following type definition to model color:
type Color {
Red
Green
Blue
}
This call of try_uniform
will produce a generator wrapped in an Ok
:
let assert Ok(color_1) = random.try_uniform([Red, Green, Blue])
let color_2 = random.uniform(Red, [Green, Blue])
The generators color_1
and color_2
will behave exactly the same.
pub fn try_weighted(
options: List(#(Float, a)),
) -> Result(Generator(a), Nil)
This function works exactly like weighted
but will return an Error(Nil)
if the provided argument is an empty list since the generator wouldn’t be
able to produce any value in that case.
It generates values from the given list with a weighted probability.
Examples
random.try_weighted([])
// -> Error(Nil)
For example if you consider the following type definition to model color:
type CoinFlip {
Heads
Tails
}
This call of try_weighted
will produce a generator wrapped in an Ok
:
let assert Ok(coin_1) =
random.try_weighted([#(0.75, Heads), #(0.25, Tails)])
let coin_2 = random.uniform(#(0.75, Heads), [#(0.25, Tails)])
The generators coin_1
and coin_2
will behave exactly the same.
pub fn uniform(first: a, others: List(a)) -> Generator(a)
Generates values from the given ones with an equal probability.
This generator can guarantee to produce values since it always takes at least one item (as its first argument); if it were to accept just a list of options, it could be called like this:
uniform([])
In which case it would be impossible to actually produce any value: none was provided!
Examples
Given the following type to model colors:
pub type Color {
Red
Green
Blue
}
You could write a generator that returns each color with an equal probability (~33%) each color like this:
let color = random.uniform(Red, [Green, Blue])
pub fn weighted(
first: #(Float, a),
others: List(#(Float, a)),
) -> Generator(a)
Generates values from the given ones with a weighted probability.
This generator can guarantee to produce values since it always takes at least one item (as its first argument); if it were to accept just a list of options, it could be called like this:
weighted([])
In which case it would be impossible to actually produce any value: none was provided!
Examples
Given the following type to model the outcome of a coin flip:
pub type CoinFlip {
Heads
Tails
}
You could write a generator for a loaded coin that lands on head 75% of the times like this:
let loaded_coin = random.weighted(#(Heads, 0.75), [#(Tails, 0.25)])
In this example the weights add up to 1, but you could use any number: the
weights get added up to a total
and the probability of each option is its
weight
/ total
.