5 min read

Purely functional nature of Elm leads to certain implications when used for generating random values. On the other hand, it opens up a completely new dimension for producing values of any desired shape, which is extremely useful in some cases.

This article covers the core concepts for working with Random module.

JavaScript offers Math.random as a way of producing random numbers; it does not expect a seed unlike traditional Pseudorandom number generator.

Even though Elm is compiled to JavaScript, it does not rely on the native implementation for random numbers generation. It gives you more control by offering an API for both producing random values without explicitly specifying the seed and having the option to specify the seed explicitly and preserve its state. Both ways have tradeoffs and should be used in different situations.

Random values without a Seed

Before digging deeper I recommend that you look into the official Elm Guide Effects / Random, where you will find the most basic example of Random.generate.

It is the easiest way to put your hands on random values. There are some significant tradeoffs you should be aware of. It relies on Time.now behind the scenes, which means you cannot guarantee efficient randomness if you run this command multiple times consecutively. In other words, there is a risk of getting the same value from running Random.generate multiple times within a short period of time.

A good use case for this kind of command would be generating the seed for future, more efficient, and secure random values. I have written a little seed generator, which can be used for providing a seed for future Random.step calls:

seedGenerator : Generator Seed
seedGenerator =
   Random.int Random.minInt Random.maxInt
       |> Random.map (Random.initialSeed)

Current time serves as a seed for Random.generate, and as you might know already, retrieving current time from the JavaScript is a side effect. Every value will arrive with a message. I will go ahead and define it; the generator will return value of the Seed type:

type Msg
   = Update Seed

init =
   ( { seed = Nothing }
     -- Initial command to create independent Seed.
   , Random.generate Update seedGenerator
   )

Storing the seed as a Maybe value makes a lot of sense since it is not present in the model at the very beginning. The initial application state will execute the generator and produce a message with a new seed, which will be accessible inside the update function:

update msg model =
   case msg of
       Update seed ->
           -- Save newly created Seed into state.
           ( { model | seed = Just seed }, Cmd.none )

This concludes the initial setup for using random value generators with a Seed. As I have mentioned already, Random.generate is a not statistically reliable source of random values, therefore you should avoid relying on it too much in situations when you need multiple random values at the time.

Random values with a Seed

Using Random.step might be a little hard at the start. The type definition annotation for this function suggests that you will get a tuple with your newly generated value and the next seed state for future steps:

Generator a -> Seed -> (a, Seed)

This example application will put every new random value on a stack and display that in the DOM:

I will extend the model with an additional key for saving random integers:

type alias Model =
   { seed : Maybe Seed
   , stack : List Int
   }

In the new handler for putting random values on the stack, I heavily rely on Maybe.map. It is very convenient when you want to make an impossible state impossible. In this case, I don’t want to generate any new values if the seed is missing for some reason:

update msg model =
   case msg of
       Update seed ->
           -- Preserve newly initialized Seed state.
           ( { model | seed = Just seed }, Cmd.none )

       PutRandomNumber ->
           let
               {- In case if seed was present, new model will contain the new value
                  and a new state for the seed.
               -}
               newModel : Model
               newModel =
                   model.seed
                       |> Maybe.map (Random.step (Random.int 0 10))
                       |> Maybe.map
                           (( number, seed ) ->
                               { model
                                   | seed = Just seed
                                   , stack = number :: model.stack
                               }
                           )
                       |> Maybe.withDefault model
           in
               ( newModel
               , Cmd.none
               )

In short, the new branch will generate a random integer and a new seed and update the model with those new values if the seed was present.

This concludes the basic example of Random.step usage, but there’s a lot more to learn.

Generators

You can get pretty far with Generator and define something more complex than just an integer. Let’s define a generator for producing random stats for calculating BMI:

type alias Model =
   { seed : Maybe Seed
   , stack : List BMI
   }

type alias BMI =
   { weight : Float
   , height : Float
   , bmi : Float
   }

valueGenerator : Generator BMI
valueGenerator =
   Random.map2
       (w h -> BMI w h (w / (h * h)))
       (Random.float 60 150)
       (Random.float 0.6 1.2)

Random.map allows using values from a passed generator and applying a function to the results, which is very convenient for making simple calculations, such as BMI:

You can raise the bar with Random.andThen and produce generators based on random values. This is super useful for making combinations without repeats.

Check the source of this example application on GitHub elm-examples/random-initial-seed

Conclusion

Elm offers a powerful abstraction for declarative definition of random value generators. Building values of any complex shape becomes quite simple by combining the power of Random.map.

However, it might be a little overwhelming after JavaScript or any other imperative language. Give it a chance, maybe you will need a reliable generator for custom values in your next project!

About the author

Eduard Kyvenko is a frontend lead at Dinero. He has been working with Elm for over half a year and has built a tax return and financial statements app for Dinero. You can find him on GitHub at @halfzebra.

LEAVE A REPLY

Please enter your comment!
Please enter your name here