gwg/rng/random
This package provides many building blocks that can be used to define pure generators of pseudo-random values. It is not cryptographically secure.
This is based on the great Elm implementation of Permuted Congruential Generators.
Note that the underlying algorith will work best for integers in the
inclusive range going from -2147483648 up to 2147483647. It can
generate values outside of that range, but they are “not as random”.
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 Random state as input and produces a new state
and a random value of the type described by the generator:
import gwg/rng/random
let #(roll_result, updated_state) = dice_roll |> random.step(random.new(11))
roll_result
// -> 3
The generator is completely deterministic: this means that - given the same
state - it will always produce the same results, no matter how many times
you call the step function.
step will produce an updated state that you can use for subsequent calls
to get different pseudo-random results:
let initial_state = random.new(11)
let #(first_roll, new_state) = dice_roll |> random.step(initial_state)
let #(second_roll, _) = dice_roll |> random.step(new_state)
first_roll
// -> 3
second_roll
// -> 2
pub opaque type Generator(a)
Values
pub fn constant(value: a) -> Generator(a)
Always generates the given value, no matter the state used.
Examples
let always_eleven = constant(11)
let #(eleven, _) = step(always_eleven, new(69))
eleven
// -> 11
pub fn decode(state: Int) -> Random
Decodes a random state.
Use new if you want to creates a new state.
pub fn dict(
keys keys: Generator(k),
values values: Generator(v),
of size: Int,
) -> Generator(dict.Dict(k, v))
Generates a Dict(k, v) where each key value pair is generated using the
provided generators.
Note that 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 float(from: Float, to: Float) -> Generator(Float)
Generates floating point numbers in the given inclusive range.
Examples
let probability = 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 = int(1, 6)
pub fn 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 like this:
let dice_roll = int(1, 6)
let attack_outcome = list(dice_roll, 3)
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 = int(1, 2) |> 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 sample(list: List(a), up_to n: Int) -> Generator(List(a))
Generates a random sample of up to n elements from a list. Returns an empty list if the sample size is less than or equal to 0.
Order is not random, only selection is.
Examples
let #(sample_list, _) = list.range(1, 10) |> sample(3) |> step(new(11))
sample_list
// -> [3, 6, 8]
pub fn set(
from generator: Generator(a),
of size: Int,
) -> Generator(set.Set(a))
Generates a Set(a) where each item is generated using the provided
generator.
Note that 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 shuffle(list: List(a)) -> Generator(List(a))
Generates a shuffled list from a given list.
Example
let #(shuffled_list, _) = list.range(1, 10) |> shuffle |> step(new(11))
shuffled_list
// -> [5, 1, 4, 9, 10, 7, 2, 3, 6, 8]
pub fn step(
generator: Generator(a),
state: Random,
) -> #(a, Random)
Steps a Generator(a) producing a random value of type a using the given
state as the source of randomness.
The stepping logic is completely deterministic. This means that, given a state and a generator, you’ll always get the same result.
This is why this function also returns a new state 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_yielder,
to_random_yielder, or sample instead.
Examples
let initial_state = new(11)
let #(first_roll, new_state) = dice_roll |> step(initial_state)
let #(second_roll, _) = dice_roll |> step(new_state)
first_roll
// -> 3
second_roll
// -> 2
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) {
then(generator, fn(value) {
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 constant
generator.
Code written with
thencan gain a lot in readability if you use theusesyntax, 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 <- then(generator) constant(fun(value)) }
pub fn uniform(options: List(a)) -> Result(Generator(a), Nil)
Generates values from the given ones with an equal probability.
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 (1 in 3) each color like this:
let assert Ok(color) = uniform([Red, Green, Blue])
pub fn weighted(
options: List(#(Float, a)),
) -> Result(Generator(a), Nil)
Generates values from the given ones with a weighted probability.
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 assert Ok(loaded_coin) = weighted([#(0.75, Heads), #(0.25, Tails)])
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.
pub fn weighted_sample(
options: List(#(Float, a)),
up_to n: Int,
) -> Generator(List(a))
Generates a random sample of up to n elements from a list with a weighted probability. Returns an empty list if the sample size is less than or equal to 0.
Order is not random, only selection is.
See weighted for more info.
Examples
let #(selecte_characters, _) =
[
#(0.4, "lan"),
#(0.3, "nam"),
#(0.2, "hoa"),
#(0.1, "ba"),
]
|> weighted_sample(2)
|> step(new(11))
selecte_characters
// -> ["lan", "nam"]
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.