More Random Elm

We will introduce a few features that come in handy when developing larger, more full-featured web applications in Elm: commands, ports, and extensible record types.

Example: Random Numbers

Let’s write a simple program to generate and display random numbers. We’ll use the application we wrote (as well as the package dependencies) for counting the number of mouse clicks as a starting point and then make a few tweaks.

We’ll have three kinds of messages: MouseClick for mouse clicks, Reset for when the escape key is pressed, and Noop for all other keys.

type Msg = Noop | Reset | MouseClick

As in Homework 2, we’ll use the Random.step function that uses an input Seed to help generate a value of type a and also a new Seed to be used next time we need to generate a value:

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

Therefore, in addition to the list of random numbers generated so far, our model also needs to keep track of the current Seed to use to generate the next number.

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

We need to create an initial Seed, which is what the Random.initialSeed function is for.

initialModel = { seed = Random.initialSeed 17, randomNumbers = [] }

The interesting case for update is MouseClick, where we generate and record the new number and seed.

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    Noop -> (model, Cmd.none)
    Reset -> (initialModel, Cmd.none)
    MouseClick ->
      let (i, newSeed) = Random.step (Random.int 1 10) model.seed in
      let randomNumbers = i :: model.randomNumbers in
      ({ seed = newSeed, randomNumbers = randomNumbers }, Cmd.none)

Finally, our view function displays the list of numbers.

view : Model -> Html Msg
view model =
  let style = ... in
  let display =
    Html.text ("Random Numbers: " ++ toString (List.reverse model.randomNumbers))
  in
  Html.div [style] [display]

If we try out the resulting application NotSoRandom.html, we see that it is not so random; the same sequence of numbers is generated every time. That’s because we always use the same initial seed (17), and the generation process is deterministic once this seed is chosen. (As suggested by the documentation for Random.initialSeed, we could use the current time (i.e. Time.now) as a proxy for a random integer. But we will use an alternative and more direct approach below.)

Commands

The Random library provides a different way to generate random values, which does not require explicitly threading seed values around:

generate : (a -> msg) -> Generator a -> Cmd msg

Notice the Cmd in the output type; this denotes an outgoing message to do something (in this case, generate a random value of type a) and then wrap the result in a message (in this case, computed by the function of type a -> msg). This something may take a long time to finish, but if and when it does, the resulting message will get fed through the update function as usual. As previewed before, commands allow programs to send outgoing messages so that they can produce other effects besides just generating HTML output.

We no longer need to track a seed value.

type alias Model = { randomNumbers: List Int }

But now we need a new kind of message, which we call RandomNumber, in addition to MouseClick

type Msg = Noop | Reset | MouseClick | RandomNumber Int

… because MouseClick will initiate the command to generate a random number and RandomNumber will contain the result of that completed command.

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    Noop -> (model, Cmd.none)
    Reset -> (initialModel, Cmd.none)
    MouseClick -> (model, Random.generate RandomNumber (Random.int 1 10))
    RandomNumber i -> ({ randomNumbers = i :: model.randomNumbers }, Cmd.none)

The resulting application MoreRandom.html generates a different sequence every time it is loaded.

Ports

We have now seen how the Random library uses outgoing messages (via commands), and earlier we saw how Mouse and Keyboard use several kinds of incoming messages (via subscriptions).

Ports allow us to define new kinds of outgoing messages — so that Elm code can call JavaScript code — and new kinds of incoming messages — so that JavaScript code can call Elm code.

As an example, we will extend our running example so that it remembers the numbers that have been generated across different visits to the HTML page. In particular, we define two JavaScript functions for reading and writing our randomly generated numbers to HTML5 Local Storage that we want to plug into our Elm application. The value of type List Int in our Elm application will be automatically converted to an array of numbers in JavaScript when crossing the language boundary. In the JavaScript code below, we convert arrays to and from strings so that they can be saved in the local store.

function loadNums() {
  var s = localStorage.getItem("randomNumbers");
  var nums = s === null ? [] : JSON.parse(s);
  return nums;
}

function saveNums(nums) {
  var s = JSON.stringify(nums);
  localStorage.setItem("randomNumbers", s);
}

To build an application that mixes Elm and JavaScript, first we compile the Elm code (MoreRandomWithMemory.elm) to JavaScript (MoreRandomWithMemory.js) rather than HTML:

elm-make MoreRandomWithMemory.elm --output=MoreRandomWithMemory.js

Then we write an HTML file (MoreRandomWithMemory.html) that includes the generated JavaScript (via <script src="...">) :

<body>
  <div id="main"></div>
  <script src="MoreRandomWithMemory.js"></script>
  <script>
    var node = document.getElementById('main');
    var app = Elm.MoreRandomWithMemory.embed(node);

    function loadNums() { ... }
    function saveNums() { ... }
  </script>
</body>

This HTML page should behave like the one when generating .html from elm-make, and when using elm-reactor.

Next, we define ports on the Elm side. We use the keyword port to tell Elm that we are going to define some ports in this module.

port module MoreRandomWithMemory exposing (main)

We define several outgoing ports (functions that send values to JavaScript via commands) and incoming ports (functions that take values from JavaScript via subscriptions):

port requestNumbers : () -> Cmd msg
port receiveNumbers : (List Int -> msg) -> Sub msg

port clearNumbers : () -> Cmd msg

port saveNumbers : List Int -> Cmd msg

These port signatures are organized into three logical operations for interacting with local storage: (1) loading the saved numbers, (2) clearing the save numbers, and (3) updating the saved numbers. Notice how the first operation is defined as a pair of ports, one to initiate the request and one to receive the result.

We add a new kind of Msg called LoadNumbers to describe the new incoming message…

type Msg = Noop | Reset | MouseClick | RandomNumber Int | LoadNumbers (List Int)

… and hook it up to the new incoming port:

subscriptions : Model -> Sub Msg
subscriptions =
  Sub.batch
    [ Mouse.clicks (always MouseClick)
    , Keyboard.downs (\keyCode -> if keyCode == 27 then Reset else Noop)
    , receiveNumbers LoadNumbers
    ]

We issue commands on the three outgoing ports in init and update:

init : (Model, Cmd Msg)
init = (initialModel, requestNumbers ())

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    Noop -> (model, Cmd.none)
    Reset -> (initialModel, clearNumbers ())
    LoadNumbers nums -> ({ randomNumbers = nums }, Cmd.none)
    MouseClick -> (model, Random.generate RandomNumber (Random.int 1 10))
    RandomNumber n ->
      let nums = n :: model.randomNumbers in
      ({ randomNumbers = nums }, saveNumbers nums)

Finally, we define JavaScript event handlers to listen (i.e. subscribe) to the three Elm outgoing ports (which are incoming from the perspective of the JavaScript code). And we send the nums from local storage to the Elm incoming port (which is outgoing from the perspective of the JavaScript):

  <script>
    ...

    app.ports.clearNumbers.subscribe(function() {
      saveNums([]);
    });

    app.ports.saveNumbers.subscribe(function(nums) {
      saveNums(nums);
    });

    app.ports.requestNumbers.subscribe(function() {
      var nums = loadNums();
      app.ports.receiveNumbers.send(nums);
    });

    ...
  </script>

All the pieces work together in MoreRandomWithMemory.html (you may want to view the page source).

Refactoring

If you’ve been following along by copying each of the versions above to start the next iteration (starting with NotSoRandom, adding the use of commands in MoreRandom, then adding the use of ports in MoreRandomWithMemory), then you’ve probably noticed, well, the amount of copied code. Let’s use this opportunity to refactor the three versions to eliminate much of the cloning.

What are the biggest opportunities for code reuse? The view function is exactly the same for all three. Several Msg data constructors (MouseClick, Reset, and Noop) are shared by all. And two of the subscriptions (for mouse clicks and keyboard presses) are shared by all.

Model and View (via Extensible Record Types)

We don’t want to use the same Model for all, because only the first version needed a Seed. We can define the following extensible record type that allows us to describe only the fields that view depends on (namely, randomNumbers, which is displayed in the HTML output).

type alias Model_ a = { a | randomNumbers : List Int }

The type variable a can be instantiated with any set of field names and types. In other words, the type variable in the definition says “forall a. such that a is a record type.” For example, the different types of models can be defined as:

type alias Model = Model_ { seed: Seed }

type alias Model = Model_ {}

Now the type of the view function can be made more general so that it can be “mixed in” to each of the three applications.

view : Model_ a -> Html msg

Controller (Messages, Update, and Subscriptions)

Although there are three Message names common to all versions, their handling in update is not always the same. Let’s have each application define their own Msg type and update function independently.

Having separate Msg types for each application complicates subscriptions a bit, since we need to know how to “wrap” the mouse clicks and keyboard presses. So, we take these three messages in as arguments. We also take an argument that allows each application to add any additional subscriptions.

makeSubscriptions : (msg, msg, msg) -> List (Sub msg) -> model -> Sub msg
makeSubscriptions (mouseClick, reset, noop) moreSubscriptions _ =
  Sub.batch <|
    [ Mouse.clicks (always mouseClick)
    , Keyboard.downs (\keyCode -> if keyCode == 27 then reset else noop)
    ] ++ moreSubscriptions

Main (Putting It All Together)

We define the following function to put the pieces together:

makeProgram : (Model_ a, Cmd msg)
           -> (msg -> Model_ a -> (Model_ a, Cmd msg))
           -> (msg, msg, msg)
           -> List (Sub msg)
           -> Program Never (Model_ a) msg
makeProgram init update commonMessages moreSubscriptions =
  Html.program
    { init = init
    , view = view
    , update = update
    , subscriptions = makeSubscriptions commonMessages moreSubscriptions
    }

Check out the refactored versions: RandomNumbersUI.elm, NotSoRandom.elm, MoreRandom.elm, and MoreRandomWithMemory.elm. Much less copied code! (As an exercise, you may want to look for ways to factor the common parts among Msg and update, and then determine whether you think the abstraction is profitable.)


Reading