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)

A state is the value that is used by the random number generation algorithm to come up with new pseudo-random values.

pub opaque type Random

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 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 new(seed: Int) -> Random

Creates a new state from a given seed.

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 then can gain a lot in readability if you use the use syntax, especially if it has some deep nesting. As an example, this is how you can rewrite the previous example taking advantage of use:

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.

Examples

Given the following type to model the outcome of a coin flip:

pub type CoinFlip {
  Heads
  Tails
  Sides
}

let assert Ok(loaded_coin) = weighted([
  #(0.49, Heads),
  #(0.49, Tails),
  #(0.02, Sides),
])

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.

Search Document