Map to the Future
Where we make random stuff with a Generator
, and work in the future
me> So… what if I wanted to get a random value…
wat> Just input the time coordinates and accelerate to 88 miles per hour…
me> I want to get a random *value* not a random *answer* 😠
wat> It kinda wasn’t. Thing is, Elm can’t give you a random value.
me> But… you told me a Generator a
was something that could give me a random a
😢
wat> Wait, don’t cry. It can five you a pseudo-random value. Which is usually random enough for most things that don’t have “crypto” in the title.
me> So, is there a function like JavaScript’s Math.random()
?
wat> What does that Math.random()
of yours do?
me> It gives me a random number each time I call it. Like…
> Math.random()
0.09275992481931072
wat> Oh, so it’s a function that returns 0.09275992481931072
every time it’s called?
me> Yes… 😄 I mean, no!!! 😅 Next time we call it, it’s going to be *another* random number. Like…
> Math.random()
0.16654248115205728> Math.random()
0.035388785049037974
wat> 🤔 So, let me get this straight: this is a function which, given the same (absence of) parameters, returns a different value each time? 😐
me> Pretty much.
wat> Oh 😶 Then no, we can’t do that in Elm. Elm’s functions are pure. No silly side-effect. Given the same parameters, they always return the same results.
me> But you said…
wat> That you could get pseudo-random stuff, yes. That’s (part of) why the *pseudo* is here actually. What it means is that, in order to get different results every time, you need to give the (pseudo-)random generation function a parameter (called a “seed”) that has to be different every time.
me> So, every time I call the function with the same seed, I get the same result, but when I call it with a different seed every time, I get different results? Well, that’s all well and fine but since I don’t know how to get a new random seed each time that does not solve my problem now, does it? 😠
wat> 😅 now let’s not get too worked up, I have good news! Let’s say I wrote a pseudo-random generation function that generates an Int
using a Seed
(that is conveniently also an int) ; I could also make it return a new Seed
that can be used for the next generation. Look:
gimmePseudoRandom : Seed -> (Seed, Int)
gimmePseudoRandom seed =
-- the code of this algorithm is not the subject here :)
-- just note that the result type (Seed, Int) is a tuple
-- that contains the new Seed first, and second the Int
-- we wanted generated> gimmePseudoRandom 12345679
(791811582, 3)
-- 3 is your random result, 791811582 is the next seed> gimmePseudoRandom 12345679
(791811582, 3)
-- given the same seed (12345679), it gives you the exact same
-- pseudo-random number (3), and the exact same seed (791811582)> gimmePseudoRandom 791811582
(9377838711, 127)
-- now using the next seed we obtained earlier, it gives you another -- pseudo-random number (127), and another next seed (9377838711)
me> Hmm… sooo… ok, it’s not random, but it’s close enough… still, I have to give it a seed, and remember the next seed for next time I want to generate a pseudo-random number… sounds complicated. 😒
wat> Sure ; that’s probably what your JavaScript does behind your back though, I’d wager that your Math.random()
function has a hidden side-effect that remembers the next seed generated every time and works with it to give you the next number…
me> Why can’t Elm be more simple, like JavaScript… 😞
wat> Don’t lament… 😃 because it kinda is!
me> But you just said?…
wat> The good people that made Elm had an idea to put that seed-y business somewhere it shouldn’t bother you too much. Like (almost) all side-effects in Elm, they are contained in the bowels of the Elm architecture, to which you can send commands (sending a Cmd msg
) and receive messages from (update : msg -> model -> (model, Cmd msg)
) so you can modify your state (and possibly send other commands).
me> So… you mean that I’ll somehow use Cmd msg
to tell the architecture to give me pseudo-random stuff? :
wat> Exactly! Now, let’s see how it really works with an example ; say I have something like this:
import Random exposing (Generator)-- this is a simple representation of a 6-faced die
type Die = One | Two | Three | Four | Five | Six-- this is also our model
type alias Model = Die-- there are two messages that affect our model
type Msg
= Throw
-- one is sent when the user wants to throw the die
| NewValue Die
-- one that gives us the new face of the die-- this is our generator for a new die value
dieGenerator : Generator Die
dieGenerator =
Random.int 1 6
|> Random.map (\intValue ->
case intValue of
1 -> One
2 -> Two
3 -> Three
4 -> Four
5 -> Five
6 -> Six
_ -> One
)
-- don't worry if it looks barbaric now, we'll explain soon :)update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
Throw ->
( model
-- the model stays as is since we don't have
-- a new value yet
, Random.generate NewValue dieGenerator
-- to get the new value, we send a command that will
-- return to us with the NewValue message containing
-- the new value
) NewValue value ->
( value
-- this time we actually change the value of the die
, Cmd.none
-- and we don't have any new command to send
)
me> So… you have made a dieGenerator
to make random Die
values… and you feed it to Random.generate
along with NewValue
… why? 😳
wat> Look at the signature for Random.generate
:
Random.generate : (value -> msg) -> Generator value-> Cmd msg
me> Ah, I see! 😃 You give a function that will make a msg
using the value
generated, a Generator
that will generate that value
, and you get a nicely wrapped Cmd
that will return with the very msg
wrapping the value
as soon as it’s generated?
wat> 👍 Indeed. And since random generation never fails, you’re sure to get your msg
back as soon as the value
it contains is generated.
me> And when I get my NewValue
message with its value inside, all I need to do is use it to change my model. Makes sense. But the dieGenerator
definition is *weird*! 😲
wat> Let’s dive in. First, we use a rather simple generator that the Random
module gives us for free:
Random.int : Int -> Int -> Generator Int
me> Yes, I guess it takes a lower and a higher bound and generates an Int
value that’s inside the bounds?
wat> Correct 😃 now let’s see if you recognize your old friend map
…
Random.map : (a -> b) -> Generator a -> Generator b
me> 🤔 Soooo, it’s one of the weird cases where map
works with something that’s promised rather than something that’s contained?
wat> Exactly. A Generator a
does not contain the a
that it will generate in the future, so you couldn’t use a simple case .. of
to get the value and work with it right here and now ; yet, with Random.map
you can use that future value to make the Generator b
you need.
me> Ok. Let me see if I get what’s happening in the transformation function:
\intValue ->
case intValue of
1 -> One
2 -> Two
3 -> Three
4 -> Four
5 -> Five
6 -> Six
_ -> One
wat> So?
me> We get the intValue
(which is an Int
since we’ve generated it with a Generator Int
) and when it’s 1
we return a One
, when it’s 2
we return a Two
… why is there a _ -> One
at the bottom though? We told Random.int
that we wanted an Int
between 1
and 6
didn’t we?
wat> Oh, that’s because the transformation function has no way to know about those bounds. All it knows is that the value will be an Int
, and a case .. of
instruction has to account for all possible values, so it needs a catch-all (_
) to ward against that possibility, even though we know it’ll never arise. Also… let’s say we could have used Random.uniform
instead for more clarity. I’ll let you have a look at the docs.
me> Oooooh, I see there’s also a Random.andThen
… Are there cases when a Generator
fails?
wat> Aha, silly human, just as map
is not just for values that are already there, andThen
is not just for failures… but that’s a story for another time.
me> Read you then 😏