arowM / tepa / Tepa.Random

This library helps you request pseudo-random values.

It is an implementation of Permuted Congruential Generators by M. E. O'Neil. It is not cryptographically secure.

Request

request : Spec a -> Tepa.Promise m a

Send a request for random values. Say you want to request random points:

import Tepa exposing (Promise)
import Tepa.Random as Random

pointX : Random.Spec Int
pointX =
    Random.int -100 100

pointY : Random.Spec Int
pointY =
    Random.int -100 100

newPoint : Promise m ( Int, Int )
newPoint =
    Tepa.succeed Tuple.pair
        |> Tepa.sync (Random.request pointX)
        |> Tepa.sync (Random.request pointY)

Each time you run the newPoint promise, it will resolve to a new 2D point like (57, 18) or (-82, 6).


type alias Spec a =
Internal.Core.RandomSpec a

A Spec is a specifcation for requesting random values. For example, here is a specification for requesting numbers between 1 and 10 inclusive:

import Tepa.Random as Random

oneToTen : Random.Spec Int
oneToTen =
    Random.int 1 10

Notice that we are not actually requesting any numbers yet! We are just constructing the request body that describes what kind of values we want. To actually get random values, you create a promise with the request function:

import Tepa exposing (Promise)
import Tepa.Random as Random

newNumber : Promise m Int
newNumber =
    Random.request oneToTen

Each time you run this promise, it checks the oneToTen specification and produces random integers between one and ten.

When testing scenarios, you specify the target request for your response with the Spec:

import Tepa.Scenario as Scenario exposing (Scenario)

respondToRandomInt : Scenario flags m e
respondToRandomInt =
    Scenario.randomResponse
        mySession
        (Scenario.textContent "Respond `1` to the first unresolved `oneToTen` request.")
        { layer = myLayer
        , spec = oneToTen
        , value = 1
        }

-- If there is no unresolved `oneToTen` request at the time, the test fails.

Primitives

int : String -> Basics.Int -> Basics.Int -> Spec Basics.Int

Request 32-bit integers in a given range.

import Tepa exposing (Promise)
import Tepa.Random as Random

singleDigit : Spec Int
singleDigit =
    Random.int "singleDigit" 0 9

closeToZero : Spec Int
closeToZero =
    Random.int "closeToZero" -5 5

anyInt : Spec Int
anyInt =
    Random.int "anyInt" Random.minInt Random.maxInt

The string argument is the ID of the Spec, and is used to check the equivalence of the two Specs in scenario testing. Be sure to specify unique IDs within the same layer.

This promise can produce values outside of the range [minInt, maxInt] but sufficient randomness is not guaranteed.

This is the TEPA version of int.

float : String -> Basics.Float -> Basics.Float -> Spec Basics.Float

import Tepa.Random as Random

probability : Random.Spec Float
probability =
    Random.float "probability" 0 1

The probability specification specifies values between zero and one with a uniform distribution. Say it requests a value p. We can then check if p < 0.4 if we want something to happen 40% of the time.

The string argument is the ID of the Spec, and is used to check the equivalence of the two Specs in scenario testing. Be sure to specify unique IDs within the same layer.

This is the TEPA version of float.

uniform : String -> a -> List a -> Spec a

Request values with equal probability. Say we want a random suit for some cards:

import Tepa.Random as Spec

type Suit
    = Diamond
    | Club
    | Heart
    | Spade

suit : Random.Spec Suit
suit =
    Random.uniform "suit" Diamond [ Club, Heart, Spade ]

That requests all Suit values with equal probability, 25% each.

The string argument is the ID of the Spec, and is used to check the equivalence of the two Specs in scenario testing. Be sure to specify unique IDs within the same layer.

Note: Why not have uniform : String -> List a -> Spec a as the API? It looks a little prettier in code, but it leads to an awkward question. What do you do with uniform identifier []? How can it request an Int or Float? The current API guarantees that we always have at least one value, so we never run into that question!

weighted : String -> ( Basics.Float, a ) -> List ( Basics.Float, a ) -> Spec a

Request values with a weighted probability. Say we want to simulate a loaded die that lands on ⚄ and ⚅ more often than the other faces:

import Tepa.Random as Random

type Face
    = One
    | Two
    | Three
    | Four
    | Five
    | Six

roll : Random.Sepc Face
roll =
    Random.weighted "roll"
        ( 10, One )
        [ ( 10, Two )
        , ( 10, Three )
        , ( 10, Four )
        , ( 20, Five )
        , ( 40, Six )
        ]

So there is a 40% chance of getting Six, a 20% chance of getting Five, and then a 10% chance for each of the remaining faces.

The string argument is the ID of the Spec, and is used to check the equivalence of the two Specs in scenario testing. Be sure to specify unique IDs within the same layer.

Note: I made the weights add up to 100, but that is not necessary. I always add up your weights into a total, and from there, the probability of each case is weight / total. Negative weights do not really make sense, so I just flip them to be positive.

Constants

maxInt : Basics.Int

Alias for Random.maxInt.

The underlying algorithm works well in a specific range of integers. It can request values outside of that range, but they are “not as random”.

The maxInt that works well is 2147483647.

minInt : Basics.Int

Alias for Random.minInt.

The underlying algorithm works well in a specific range of integers. It can request values outside of that range, but they are “not as random”.

The minInt that works well is -2147483648.