Toolbox.Random (toolbox v1.1.0)

Functions for working with deterministic (pseudo)randomness in scenarios.

In some use cases, scenarios need to behave “randomly”, for example, when a scenario serves as a demo events generator. Scenarios are by design deterministic which means that they can only behave randomly in the sense that they make certain decisions “on their own” (without the programmer explicitly choosing the outcome) but every time a scenario run is repeated, the same “random” decisions are made. To make this possible, the seed of the random number generator has to be always initialized to the same value and managed in the way that it is preserved throughout the run. And that is what this module helps with.

The module wraps around Erlang :rand module for random number generation. Functions in the :rand module use process dictionary for storing the random number generator state (i.e. the seed). To be able to restore a run, the random number generator state is also kept in the state of a unit in an opaque field called __rand_seed.

There are three steps (functions of this module) in order to use randomness in scenarios. First, you have to initialize the unit that uses randomness with init/2. And then, every time before you want to use randomness, you have to call load/1 with the particular unit. And then store the new seed with save/1. An example is given below. See the documentation for each of the functions for more detailed information.

example

Example

defmodule Scenarios.MyScenario.MyTemplate
  @behaviour Toolbox.Scenario.Template.StageBased

  alias Toolbox.Random

  def instances do
    # singleton unit
    [{ :unit,"generator", %{} }]
  end

  def init(_start_from, unit) do
    unit = Random.init(unit, 293732)
    {:ok, [], unit}
  end

  def handle_message(msg, unit) do
    Random.load(unit)
    ... do something that uses :rand calls
    ... (e.g. :rand.uniform/0 or Enum.random/1)
    {:noreply, oas, Random.save(unit)}
  end
end

Link to this section Summary

Functions

Shortcut for init/2 called with __MODULE__ as the seed.

Initializes a unit for random number generation.

Loads a seed from the unit's state into the process dictionary.

Stores the random generator state in the unit's state.

Link to this section Types

Link to this type

input_seed()

@type input_seed() :: term()
@type seed() :: :rand.seed()

Link to this section Functions

Link to this macro

init(unit)

(macro)

Shortcut for init/2 called with __MODULE__ as the seed.

It is a bit shorter, but on the other hand you have to require Toolbox.Random to be able to use this macro. Beware that the generated values will differ when you rename your template module.

Link to this function

init(unit, seed)

Initializes a unit for random number generation.

You need to provide an initial seed which may be any term, in particular any integer, string or atom. The value doesn't matter (it only affects the randomly generated values) as long as it is constant. Typically, you would use this function inside the init/2 callback of your template.

In case you are using a singleton unit, you may, for example, hardcode an integer here or use __MODULE__ as the initial seed:

def init(_start_from, unit) do
  unit = Random.init(unit, __MODULE__)
  {:ok, [], unit}
end

If you have multiple units (e.g. unit for each person), you may initialize each unit with a different seed (e.g. the ID of that person):

def init(_start_from, unit) do
  unit = Random.init(unit, unit.attributes.id)
  {:ok, [], unit}
end

This function saves the initial seed into the unit's state under an opaque field called __rand_seed and return the modified unit. Therefore, to use randomness, the unit's state has to be a map.

The function also sets the seed in the process dictionary so you can start using randomness right away. But you will probably want to do so later in the handle_message/2 callback.

@spec load(Toolbox.Runtime.Stage.Unit.t()) :: :ok

Loads a seed from the unit's state into the process dictionary.

Typically, you would call this function in the beginning of a handle_message/2 callback that deals with randomness. It is sufficient to call it once per invocation of the callback.

example

Example

def handle_message(msg, unit) do
  Random.load(unit)

  person = Enum.random(unit.state.people)
  room = Enum.random(unit.state.rooms)
  # generate an event that $person went into $room

  {:noreply, oas, Random.save(unit)}
end

Stores the random generator state in the unit's state.

Typically, you would use this function after generating random values (and thus modifying the generator's seed) in the handle_message/2 callback.

The function returns the modified unit.