Randomness in Elm
Explaining Elm Generators by Example
When writing Elm code nearly every line of code you write will be inside of a pure function. But that begs the question, just how do you invoke the equivalent of
Math.random() (an impure function) in Elm? Surely even a pure functional language can still use randomness, right?
Indeed it can! Let’s start off by exploring how randomness can be achieved in Elm by looking into the bottom type behind it all, generators.
Generators are behind all randomness in Elm. A generator is a type that describes how to produce random values.
A generator is a type that describes how to produce random values.
For example, here is a generator that describes a random integer from 0 to 100.
Here is another generator that leverages Random.map to describe a randomly selected suit for a deck of playing cards.
Remember, suitGenerator and zeroTo100Generator are values of type Generator. A generator on its own cannot produce random values. For that you need to pass the generator to one of two functions:
Generate vs Step
Generate and step are the two functions that allow you to produce random values in the purely functional Elm. I like to think of them as randomness as a side effect and randomness as a pure function.
Randomness as a side effect
Let’s start with generate’s function annotation.
Generate is a function that takes a Generator and a mapping function and produces a
Cmd msg. In Elm, Cmd’s are used to describe side effects that are then performed by the Elm runtime.
Note that there are a few moving pieces in this example.
- When the user clicks the button a
Msgis dispatched from
- The Elm runtime passes that
updatefunction has a branch for that
Msgand it produces a
Random.generate. This tells the Elm runtime that we want to produce a side effect.
- The Elm runtime produces a random value and sends a different
Msgback to the
updatefunction which then updates the model with the random value.
Randomness as a pure function
Step, on the other hand, has a different signature.
The step function takes a generator and a seed and produces a tuple containing the randomly produced value and a new seed. Contrary to
generate, where randomness is treated as a side effect and handed to the Elm runtime, with
step randomness is treated as a pure function.
This seems bizarre since calling
Which one is better?
Given these two options you’re probably wondering which is better between
step. Does it matter if you’re producing one random value? What about multiple random values? Or what about a random value that depends on the result of another random value?
The short answer is
generate is probably better.
The longer answer is, from personal experience writing an app where you have to manage your own random seeds with
step can be a huge pain and prone to errors. Realize that when you call
step with a seed it gives you another seed that you can pass to subsequent
step invocations. That means that a potentially globally tracked seed is leaking into the depths of your functions.
generate is a pretty good option. Even if you need to produce multiple random values that depend on each other you can get away with a single call to
generate by composing generators.
Building generators by example
Let’s go over a few more examples to help give you a better idea of how to build larger generators that produce more interesting random values.
Random.map we can take the output of one or more generators and transform it into a generator for a different value. Let’s create a generator that will return a Jump message most of the time and a Crouch message otherwise.
Random.andThen takes the random value from one generator and returns a new generator. This is useful for producing different generators depending on the result of another random value.
Let’s say I want to randomly produce two different values. Fifty percent of the time I want to produce a random number between 1 and 10 and fifty percent of the time I want a random number between 11 and 20. This is a case where one random value depends on another.
For this last example I’m going to show the most advanced example of randomness in Elm that I can think of. It is not for the faint of heart. Last year I wrote a package called elm-genetic that provides some of the basic boilerplate for using a genetic algorithm.
For the purposes of this post, a genetic algorithm uses quite a bit of randomness to mimic the process of biological evolution to solve a problem.
There are many elements of randomness involved:
- What random solutions are in Generation 0
- Which random traits are mutated
- Given a random trait, how that trait will be randomly mutated
- Which random parents “breed” together
The actual algorithm has a fair amount of boilerplate (hence the idea to extract the common parts to a package) but the algorithm needs to know about a few pieces of information.
Right off the bat it needs to know two pieces of information that involve randomness. It needs to know how to generate the DNA for a random solution (to populate Generation 0). It also need to know how to mutate the DNA of a given solution.
And the code for
Actually, there’s a lot more to it than that.
Both of these Generators are provided to the genetic algorithm but like what was mentioned earlier the genetic algorithm still has other elements of randomness too. If we pop open the hood of the elm-genetic package we’ll see a lot of generator composition.
For example, this code shows a generator that ultimately randomly “breeds” two parents to produce a child, randomly mutates that child’s DNA, and based on the outcome evaluates how good its DNA is.
And the code to generate an initial random population of possible solutions..
You can see this live at http://ckoster22.github.io/genetic/bugsbunny.html (it takes a while to run)
And there are a few more examples in the source.
If this is your first go with randomness in Elm that file will be a bit overwhelming so if you have any questions I’m available on Elm slack as charliek. Feel free to ping me with your questions.