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.
type alias Model =
{ randomNumbers : List Int }
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]
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
.
(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 Msg
s:
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.
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.
(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
.
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>
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.
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...
elm.json
RandomNums1.elm
RandomNums2.elm
RandomNums3.elm
RandomNums4.elm
and RandomNums4.html
RandomNums5.elm
and RandomNums5.html
RandomNums6.elm
and RandomNums6.html
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.