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 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

The Random library generates pseudo-random values. (You may need to elm install elm/random.) Given an initial Seed ...

initialSeed : Int -> Seed

... the Random.step function uses the Seed to 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
  }

initModel =
  { 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
    styles =
      ...
    display =
      Html.text <|
        "Random Numbers: " ++ Debug.toString (List.reverse model.randomNumbers)
  in
  Html.div (List.map (\(k, v) -> Attr.style k v) styles) [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.

Instead, we will use an alternative and more direct approach, which does not require explicitly threading seed values around:

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

Notice the Cmd in the output type...

Commands

As mentioned last time, commands allow programs to send outgoing messages so that they can produce other effects besides just generating HTML output. The type Cmd a describes outgoing messages message to request something be done which (if and) when completed, produces a value of type a. (In contrast to commands, subscriptions Sub a are for incoming messages.)

In the case of Random.generate, the requested "something" is a random value of type a, to be wrapped into a Msg by calling the (a -> Msg) function argument. The resulting Msg will be fed through the update function as usual.

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 ->
      (initModel, 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.

JavaScript Interop with Ports

We have seen how the Random library exposes outgoing messages (via commands), and earlier we saw how Browser.Events exposes incoming messages (via subscriptions).

So that Elm code can interoperate with raw HTML and JavaScript, ports allow the definition of 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 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 (say, MoreRandomWithMemory.elm) to JavaScript (say, MoreRandomWithMemory.js) rather than HTML:

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

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

<body>
  <div id="elm"></div>
  <script src="MoreRandomWithMemory.js"></script>
  <script>
    var app = Elm.MoreRandomWithMemory.init({
      node: document.getElementById('elm')
    });

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

This HTML page is the entry point of the application, rather than index.html as generated by elm make when --output is not specified.

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 ReceiveNumbers to describe the new incoming message...

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

... and hook it up to the new incoming port:

subscriptions : Model -> Sub Msg
subscriptions =
  Sub.batch
    [ Browser.Events.onMouseDown (Decode.succeed MouseClick)
    , Browser.Events.onKeyDown
        (Decode.map (\key -> if key == "Escape" then Reset else Noop) keyDecoder)
    , receiveNumbers ReceiveNumbers
    ]

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

init : Flags -> (Model, Cmd Msg)
init () = (initialModel, requestNumbers ())

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

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.elm, adding the use of commands in MoreRandom.elm, then adding the use of ports in MoreRandomWithMemory.elm), 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. We'll define a new RandomNumbersUI module, and then use it in the three different versions of the application.

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 <|
    [ Browser.Events.onMouseDown (Decode.succeed mouseClick)
    , Browser.Events.onKeyDown
        (Decode.map (\key -> if key == "Escape" then reset else noop) keyDecoder)
    ] ++ moreSubscriptions

Main (Putting It All Together)

We define the following function to put the pieces together:

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

Check out the refactored versions:

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