Mimicking Choice with Delayed Generators

by tkshill

Creating Delayed Random Generator Commands in Elm


In this brief article, we look at using Tasks, Generators and Msg constructors in Elm to similate the effect of a computer opponent in a game, "thinking" about a choice.

Although the implications and paradigms referred to are relatively language agnostic, the actual solution implementation is written in the Elm, and the article assumes some knowledge of the programming language and the basics of the Model-View-Update style of User Interface design.

Tl;dr - If you'd rather just go straight to the working solution, just check out this Ellie.

Context

Sometimes in programming, we want to intentionally delay the effect, or response from a particular computation. One such case is when you want to simulate to the user than the computer/app device they're interacting with is thinking. This concept of a thinking or pondering machine has been especially important to me as I've been working on my implementation of Quarto, a sort've advanced tic tac toe variant.

In Quarto, players can challenge themselves against a computer opponent, who at the moment, plays randomly available pieces randomly on the board.

Making simple generators

First, let's look at the stardard random generator.

To illustrate our example today, we're going to be using the concept of playing cards. Below I've added a snippet of code from the elm-lang Random cards example, trimming way the parts unnecessary to our exploration today.

Unlike in other less strict languages like python or javascript, Elm can't simply produce a random number on the fly. All Elm functions are pure, meaning the same input should produce the same output every time. By definiton, the concept of randomness is inherently impure, since a random when given the same inputs, produces many different outputs. So we need a slightly more sophisticated method to get our random numbers.

The Model type

-- MODEL
type Card
  = Ace
  | Two
  | Three
  | Four
  | Five
  | Six
  | Seven
  | Eight
  | Nine
  | Ten
  | Jack
  | Queen
  | King

type alias Model =
  { card : Card
  }

Above we see that we define a union type called Card, with each possible value of the Card type representing the potential value of a real life playing card.

Our model, the state of our application, is a record with a single field, card, which stores any one possible value of our Card type.

The Msg type

-- UPDATE
type Msg
  = Draw
  | NewCard Card

Next we define the messages that can be passed around our app with a type called Msg. In Elm, passing messages are the only way to trigger updates to the app.

  • Our first Msg, Draw is what we'll use to trigger our card generator function.
  • Our second Msg, NewCard Card is the message that our random card generator will return once it's found a card from the list. Note that this message has Card as an associated type.

Next let's look at our actual randomness function.

The Random Generator type

-- Our generator function
cardGenerator : Random.Generator Card
cardGenerator =
  Random.uniform Ace
    [ Two
    , Three
    , Four
    , Five
    , Six
    , Seven
    , Eight
    , Nine
    , Ten
    , Jack
    , Queen
    , King
    ]

Here we define a function cardGenerator that uses the elm/Random API to make a random generator. Now, we don't really need to get into how generators work behind the scenes (fancy maths), we just have to understand that the Random.uniform function is a function that accept some data type a, where a can be anything, and a List of a and will return a random Generator a. So in this scenario, a is our Card type.

Now so far, we have a Model that holds cards, a Generator that produces cards, and a Msg that passes cards, but now we need to put them together.

The Update function

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    Draw ->
      ( model
      , Random.generate NewCard cardGenerator
      )

    NewCard newCard ->
      ( Model newCard
      , Cmd.none
      )

So finally we have the update function. In Elm, the update function is the only way we can update the model. Note the type signature (always start with the type signature), Msg -> Model -> (Model, Cmd Msg).

Update accepts a Msg and the existing Model, and returns an updated model and a Cmd, which is basically some external task for the app to perform.

So let's break down our update function

  • If a Draw message is passed to the application, the model doesn't change but a command is sent using the
    Random.generate function.
    • Random.generate accepts a function that takes some type a and returns a Msg, then a Generator of that a, runs the generator, and passes the returns value to the msg constructor.
    • The reason why Random.generate is a cmd is because the Elm runtime has to reach outside the app state to get a number to seed the random function.
  • Once the generator produces it's value, it turns it to the NewCard branch of the update function, where our model is updated with that newly generated value.

If you want to see a the full version of how this works with the rest of the bells and whistles, you can check it out here.

The Delay

So now we have a working random generator, and with the speed of modern computers, the generation happens pretty quick. Too quick. Our computer is too smart! Or more honestly, it's really troubling for the human brain to process that a sequence has happened that it can't even see. So how do we pause an application?

We put it to sleep!

Let's make a delay function that can delay the progression of the application. What do we need to do this.

Process.Sleep

Firstly, we need the Process.sleep function. Sleep accepts a Float and returns a Task. For now, let's treat a task as just a promise to do a thing. We can request that Elm perform tasks, and tasks could either succeed or fail. Typically when they succeed, they return a value to the app through our delightful Msg type again. Sleep however, returns the type () unit, or essentially nothing. It just y'know, sleeps.

So sleep 1000.00 produces a task that "sleeps" for 1000 milliseconds.

Time.Now

The next thing we'll need is the Time.now function, which like, sleep, produces a task, but also returns a Posix time value, representing the current time. Posix time isn't necessary useful for us, so we'll also use Time.posixToMillis to convert the time from Time.now to an integer.

Random.step

The final unique function we're gonna look at is the Random.step.

Manual Generators

Earlier on, we used Random.generate function to run our generator. But now, we want to chain a generator into a sleep process. Unfortunately, because of this we can't use generate anymore, since Elm commands are run in parallel, and cant be chained the way we want.

Fortunately, there's a way to produce a random function without using a command, and that's by passing in the inital seed ourserlves. Given a generator and an initial seed, the step function can run our generator and produce a random value (as well as a new seed in case we need to keep generating random numbers).

Using all this knowledge, we can define first, a function generateCard

generateCard : Int -> Msg
generateCard seedNum =
  seedNum
  |> Random.initialSeed
  |> Random.step cardGenerator
  |> (\( value, _ ) -> NewCard value)
  • This function accepts an Int and uses the step function to make a new value (no commands required) and then creates our NewCard msg with that value.
  • Note we make use of another Random function, initialSeed, which when given an Int, returns a Seed type that can be consumed by the step function.
  • Step returns a tuple of the generated value and a new seed. We keep the value, and pass it into the NewCard constructor.

And then we can finally define a delay function:

delayGenerator : Cmd Msg
delayGenerator =
    Process.sleep 1000.00
        |> Task.andThen (\_ -> Time.now)
        |> Task.perform (Time.posixToMillis >> generateCard)
  • Our delayGenerator function chains our sleep Task into the Time.now Task using a helper functon andThen, who's implementation is beyond the scope of this article.

We can essentially read this function as

  • Run the sleep task and wait for three seconds
  • Ignore the value sleep passes back, and run the now function to get the current time
  • call Time.perform (similar to Random.generate, perform can resolve these promises to do tasks into actual values
  • perform will pass the posix value from now to posixToMillis to get an Int.
  • And finally that integer is passed to our generateCard function, and pops out at the end a random card, 1000.00 milliseconds after delay is called.

And the best part is, our new update function doesn't change much at all.

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    Draw ->
      ( { model | status = "Choosing a card..." }
      , delayGenerator
      )

    NewCard newCard ->
      ( Model newCard "Card chosen. Draw again?"
      , Cmd.none

Note that all we had to do was change our Cmd Msg in the Draw branch from Random.generate to our new delayGenerator Task maker.

Wrap Up

And that's it. I recognize that this might not all make perfect sense from the beginning, and that's okay. But hopefully I at least brought some context to how you can build these types of chains of commands in a logical fashion, combining matching functions and types.

I've attached a full example here that shows the whole thing in action. It's also available in this gist

I also highly recommend reading the documentation on random functions and tasks to get more context.

Also, if you want to see this type of concept out in the wild, I'm going to shamelessly plug my small game app Quarto where you can look at this type of code in action, and maybe even contribute if you'd like. I've been offering pair programming sessions whenever I can to folks looking to learn elm and contribute to open source.

Thank you for reading :)

Created 2 weeks ago | Updated 2 weeks ago

Comments

GistLog © 2020
Brought to you by the lovely humans at Tighten