More Random Elm

Our running application will be 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, with a few tweaks. Then we will work through several variations as an excuse to introduce several features that may come in handy as you develop larger, more full-featured web applications in Elm: commands, tasks, flags, ports, and data visualizations.

Model

type alias Model =
  { randomNumbers : List Int }

View

The view function displays the list of numbers.

view : Model -> Html Msg
view model =
  let
    styles =
      ...
    display =
      Html.text (Debug.toString (List.reverse model.randomNumbers))
  in
  Html.div (List.map (\(k, v) -> Attr.style k v) styles) [display]

Update

We'll start with three messages:

type Msg
  = MouseDown
  | EscapeKeyDown
  | OtherKeyDown

subscriptions : Model -> Sub Msg
subscriptions model =
  Sub.batch
    [ Browser.Events.onMouseDown
        (Decode.succeed MouseDown)
    , Browser.Events.onKeyDown
        (Decode.map
          (\key -> if key == "Escape" then EscapeKeyDown else OtherKeyDown)
          (Decode.field "key" Decode.string))
    ]

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    EscapeKeyDown ->
      (initModel, Cmd.none)
    OtherKeyDown ->
      (model, Cmd.none)
    MouseDown ->
      ...

We want a random number on MouseDown.

Commands (version 1)

(You may need to run elm install elm/random in your project directory.)

The Random.generate function produces a value of type T in an application which operates on Msgs:

generate : {- forall a msg. -}   (a -> msg) -> Generator a -> Cmd msg
generate   {-        T Msg. -} : (T -> Msg) -> Generator T -> Cmd Msg

First, we need to build up a Generator T (the second argument), by starting with primitive generators...

int : Int -> Int -> Generator Int
uniform : a -> List a -> Generator a     -- (a, List a) ~= "non-empty list"
...

... and then transforming them into more complex generators, as needed for our goal type T:

list : Int -> Generator a -> Generator (List a)
map : (a -> b) -> Generator a -> Generator b
...

Second, we provide a function of type (T -> Msg) (the second argument) that decides how to transform a randomly generated value into a message for our application.

The result of generate is a Cmd Msg... what's that? 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 Msg describes an outgoing message that requests something to be done which (if and when completed) produces a value of type Msg. (Subscriptions Sub Msg are for incoming messages from sources outside the Elm application.)

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

So, we will add a new kind of message, which we call RandomNumber (what's the type of this data constructor?):

type Msg
  = ...
  | RandomNumber Int

MouseDown will initiate the command to generate a random number, and RandomNumber will wrap the result of the completed command.

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    OtherKeyDown ->
      (model, Cmd.none)
    EscapeKeyDown ->
      (initModel, Cmd.none)
    MouseDown ->
      (model, Random.generate RandomNumber (Random.int 1 10))
    RandomNumber i ->
      ({ randomNumbers = i :: model.randomNumbers }, Cmd.none)

Compile...

% elm make RandomNums1.elm --output=RandomNums1.html

... and try out the resulting application RandomNums1.html, which generates a different sequence every time it is loaded.

Seeds (Version 2)

What if we want a random sequence, but in a way that we can reproduce the same sequence again? 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)

Notice that this does not use commands at all. A particular Seed completely determines the behavior of step, so it is a pure function — no messages, i.e., no side-effects.

The second component of the output is a new Seed value, to be used next we call step. Therefore, in addition to the list of random numbers generated so far, the 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 = []
  }

Because commands are not involved, we no longer need to "split" random number generation across MouseDown and RandomNumber; we delete the latter kind of Msg.

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

Try out the resulting application RandomNums2.html and notice that 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.

Let's suppose we want this ability to reproduce randomly generated values (for debugging in "development" mode) but that we also want to be able to randomly choose the initial seed (for actual use in "deployment" mode).

As suggested by the documentation for Random.initialSeed, we could (a) use the current time (via Time.now) to serve as a random seed or (b) use JavaScript to generate a random seed and send it to Elm using flags. Next, we will try each of these two approaches.

Tasks (Version 3)

(You may need to run elm install elm/time in your project directory.)

Time.now : {- forall x. -} Task x Posix

What's a Task? Remember the type Maybe a to represent maybe a value (Just a) and otherwise "error" (Nothing)? And Result x a for maybe a value (Ok a) and otherwise an "error" value Err x, which carries some information x : x (e.g. a String error message)? Task is a mash-up of Cmd and Result:

Task x a ~= Cmd (Result x a)

Indeed, to run a task, we convert it to a Cmd:

Task.attempt : (Result x a -> msg) -> Task x a -> Cmd msg

(Task also has some other library functions that the "raw" Cmd library does not.)

If a task, such as Time.now...

Time.now : {- forall x. -}   Task x     Posix
Time.now   {-   Never   -} : Task Never Posix

... "never fails", then we don't need to handle the Err case:

Task.perform : (a -> msg) -> Task Never a -> Cmd msg

So, we "poll" the current time on startup and then transform it with a Posix -> Msg function into a new kind of Msg:

type Msg
  = ...
  | RandomSeed Seed

init : Flags -> (Model, Cmd Msg)
init () =
  let
    posixToMsg : Posix -> Msg
    posixToMsg p =
      ...
  in
    (initModel, Task.perform posixToMsg Time.now)

There are lots of ways we can write posixToMsg (cf. Infix Operators):

posixToMsg p =
  RandomSeed (Random.initialSeed (Time.posixToMillis p))

or

posixToMsg p =
  RandomSeed <| Random.initialSeed <| Time.posixToMillis p

or

posixToMsg =
     RandomSeed
  << Random.initialSeed
  << Time.posixToMillis

or

posixToMsg p =
  p |> Time.posixToMillis
    |> Random.initialSeed
    |> RandomSeed

or

posixToMsg =
     Time.posixToMillis
  >> Random.initialSeed
  >> RandomSeed

(The forward pipeline via reverse application (|>) is my personal favorite.)

Once the seed is ready, we simply store it in the Model.

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    ...
    RandomSeed seed ->
      ({ model | seed = seed }, Cmd.none)

Because we will always receive the seed before the user starts clicking (right?), we'll keep the seed field in Model as a Seed — rather than a Maybe Seed — with an initial hard-coded value. This keeps the MouseDown case, which calls Random.step, the same as before.

Try out the resulting application RandomNums3.html.

JavaScript Interop: Flags (Version 4)

One way for Elm code to interoperate with raw HTML and JavaScript is to use flags, which are JavaScript values provided when an Elm application is initialized (via init).

Starting with our Version 2 code, we make a couple small tweaks to take an integer timeNow flag, which will be computed via the JavaScript function Date.now():

type alias Flags =
  Int

init : Flags -> (Model, Cmd Msg)
init timeNow =
  let
    initModel =
      { seed = Random.initialSeed timeNow
      , randomNumbers = []
      }
  in
    (initModel, Cmd.none)

Rather than compiling our Elm code directly to HTML, this time we compile it to JavaScript...

% elm make RandomNums4.elm --output=RandomNums4.js

... and then directly write the following HTML page (RandomNums4.html; viewing the page source will show the same code as below). This page loads the generated JavaScript file, which defines the Elm.RandomNums4.init function to embed the Elm application into a given HTML node, called "elm". The flags field is where we pass a JavaScript value (corresponding to our application-specific Flags type) to Elm.

<!DOCTYPE HTML>
<html>
<head>
  <meta charset="UTF-8">
  <title>RandomNums4</title>
  <script src="RandomNums4.js"></script>
</head>

<body>
  <div id="elm"></div>
  <script>
  var app = Elm.RandomNums4.init({
    node: document.getElementById('elm'),
    flags: Date.now()
  });
  </script>
</body>
</html>

JavaScript Interop: Ports (Version 5)

We have seen how the Random and Time libraries expose outgoing messages (via commands and tasks), and earlier we saw how Browser.Events exposes incoming messages (via subscriptions). To allow custom, user-defined interoperation between Elm and JavaScript code, the ports mechanism 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, let's extend our running example to remember the numbers that have been generated across different visits to the HTML page.

First, 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.

<html>
<head>
  ...
  <script src="RandomNums5.js"></script>
</head>
<body>
  <script>
  ...

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

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

  </script>
</body>

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 RandomNums5 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 saveNumbers : List Int -> Cmd msg

port loadNumbersRequest : () -> Cmd msg
port loadNumbersReceive : (List Int -> msg) -> Sub msg

These port signatures are organized into three logical operations for interacting with local storage: (1) saving the random numbers to local storage, and (2) loading the saved numbers. Notice how the latter 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 LoadNumbersReceive to describe the new incoming message...

type Msg
  = MouseDown
  | EscapeKeyDown
  | OtherKeyDown
  | LoadNumbersReceive (List Int)

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

subscriptions : Model -> Sub Msg
subscriptions =
  Sub.batch
    [ ...
    , loadNumbersReceive LoadNumbersReceive
    ]

We handle the new kind of message, and we issue commands on the three outgoing ports in init and update:

init : Flags -> (Model, Cmd Msg)
init timeNow =
  let
    ...
  in
    (initModel, loadNumbersRequest ())

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of
    LoadNumbersReceive nums ->
      ({ model | randomNumbers = nums }, Cmd.none)
    EscapeKeyDown ->
      ({ model | randomNumbers = [] }, saveNumbers [])
    OtherKeyDown ->
      ...
    MouseDown ->
      let
        ...
      in
        (newModel, saveNumbers randomNumbers)

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.saveNumbers.subscribe(function(nums) {
    saveNumbers(nums);
  });

  app.ports.loadNumbersRequest.subscribe(function() {
    var nums = loadNumbers();
    app.ports.loadNumbersReceive.send(nums);
  });

  </script>

All the pieces work together in RandomNums5.html (take a look at the complete page source). Generate some numbers and try reloading the application.

Vega Visualizations (Version 6)

As a final embellishment, let's visualize the randomly generated numbers. Vega and Vega-Lite are powerful specification languages for defining visualizations. See tons of cools examples here and here. Vega and Vega-Lite specifications are defined in JSON (JavaScript Object Notation) format. The elm-vega and elm-vegalite libraries are Elm wrappers around Vega and Vega-Lite. Just to whet the appetite, let's generate a little bar chart for our application.

(Run elm install gicentre/elm-vegalite in your project directory.)

To embed a Vega-Lite specification in HTML/JavaScript, we start by adding a few scripts to our header...

<head>
  ...
  <script src="https://cdn.jsdelivr.net/npm/vega@5.10.1"></script>
  <script src="https://cdn.jsdelivr.net/npm/vega-lite@4.10.2"></script>
  <script src="https://cdn.jsdelivr.net/npm/vega-embed@6.5.2"></script>
  <script src="RandomNums6.js"></script>
</head>

... add a new "vis" node, where the rendering will eventually go:

<body>
  <div id="elm"></div>
  <div id="vis"></div>
  ...
</body>

Next, we need to define a function to generate a VegaLite.Spec based on the random numbers. RandomNums6.elm provides two simple such functions, called makeVis and makeVis2. Take a look at them, the Vega-Lite examples, and the elm-vegalite documentation if you want to learn more.

Then, we need a way to send the visualization to JavaScript, where the Vega rendering library can be called. A VegaLite.Spec is just a JSON value, encoded via Json.Encode.

type alias VegaLite.Spec = Json.Encode.Value

So, we define an outgoing port...

port sendVis : Json.Encode.Value -> Cmd msg

... and subscribe to in JavaScript:

app.ports.sendVis.subscribe(function(vegaLiteSpec) {
  vegaEmbed('#vis', vegaLiteSpec);
});

Finally, we need to send messages over the sendVis port. We define a helper function, makeAndSendVis, and use it in init and in update every time randomNumbers changes. Notice the use of Cmd.batch to combine commands.

makeAndSendVis : List Int -> Cmd msg
makeAndSendVis =
  sendVis << makeVis2

init : Flags -> (Model, Cmd Msg)
init timeNow =
  let
    initModel =
      { seed = Random.initialSeed timeNow
      , randomNumbers = []
      }
    initCmds =
      [ loadNumbersRequest ()
      , makeAndSendVis []
      ]
  in
    (initModel, Cmd.batch initCmds)

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
  case msg of

    LoadNumbersReceive nums ->
      ( { model | randomNumbers = nums }
      , makeAndSendVis nums
      )

    EscapeKeyDown ->
      ( { model | randomNumbers = [] }
      , Cmd.batch
          [ saveNumbers []
          , makeAndSendVis []
          ]
      )

    OtherKeyDown ->
      (model, Cmd.none)

    MouseDown ->
      let
        (i, seed) =
          Random.step (Random.int 1 10) model.seed
        randomNumbers =
          i :: model.randomNumbers
      in
        ( { seed = seed, randomNumbers = randomNumbers }
        , Cmd.batch
            [ saveNumbers randomNumbers
            , makeAndSendVis randomNumbers
            ]
        )

Check out RandomNums6.html. Trackpad drumroll please...


Reading

Source Files

You may want to run, for example, vimdiff RandomNums2.elm RandomNums4.elm or vimdiff RandomNums5.html RandomNums6.html or some such, to scrutinize the differences between versions.

Additional

  • Miscellaneous notes from last year: 2020.0417