Note: You don't need very much Elm knowledge at all to benefit from this, but equally this isn't really intended for anyone with zero knowledge of Elm. It's more for people who have just started playing around with the absolute basics of the language and are trying to understand how to approach "communication between components" (or rather, why not to think that way!)
Update on October 9th, 2018: updated for Elm 0.19
Today I was re-creating an example application in Elm (the original example was in React+Redux), and realised that I'd written the Elm example that I wish I'd had myself when I first started exploring the language, so I thought it might be worth sharing with a wider audience.
Initial setup
If you don't have Elm installed on your machine already, you might want to take a look at the installation instructions now.
With Elm installed, in a terminal at a location of your choosing, run elm init
to initialise your new project. This will create an elm.json
file for you, along with a src
directory. Note that this doesn't create the app directory for you, so make sure that you're inside the directory you actually want your application to live in at this point.
Start by creating a Main.elm
file inside of your src
directory, and open it in whichever editor you prefer (although if you want a recommendation, Visual Studio Code along with the Elm language plugin is super nice to work with), and add your module declaration to the top:
module Main exposing (..)
A quick intro to the Elm Architecture
The key to an Elm application is the Elm Architecture, which - at least in an application that doesn't talk to the outside world at all - looks like this:
Which means we need a Model
, a View
, and an Update
. But what are these?
Model
Our Model
is where we're going to define the state
of our application. The entire idea behind this example is to consider the state of our application before anything else, although we'll talk about this in a little more detail later.
View
Our view
is going to be a function that takes a Model
, and returns a representation of our DOM in Elm. Elm includes a virtual DOM, along with an HTML library to enable you to write your markup whilst taking advantage of the guarantees that Elm gives you.
Update
Our update
function is going to be responsible for handling any updates to our Model
. It's important to note that despite what the name implies, no update to the model actually happens in situ - instead, update
will return a new model, which will be passed to our view
function, allowing any necessary changes to be rendered in our application.
What we're going to build
Okay, with that out of the way, let's have a look at our task. We're going to be building a high-tech, state-of-the-art, virtual room. That room is going to contain a single door, and an alarm system. The specifications for this are:
If the alarm is armed, then it should be triggered by the door opening.
If the alarm has been triggered, then it can be disarmed, but not armed.
If the door is open, the alarm's current state can not be altered manually.
If the door is open it can be closed.
If the door is closed it can be opened or locked.
If the door is locked it can be unlocked.
So we know that we need to have a room that contains two things, each of which can be represented in a number of possible states, we know what combination of states can possibly be represented, and we know what the transitions are between the possible states. To my mind, we're looking at:
-- POSSIBLE STATES:
Door:
Locked
Closed
Opened
Alarm:
Armed
Disarmed
Triggered
Combined:
Locked + Armed
Locked + Triggered
Locked + Disarmed
Unlocked + Armed
Unlocked + Triggered
Unlocked + Disarmed
Opened + Triggered
Opened + Disarmed
-- POSSIBLE TRANSITIONS:
Door:
Closed <-> Locked
Closed <-> Opened
Alarm:
Armed -> Triggered
Triggered -> Disarmed
Armed <-> Disarmed
But what about the entire application? In a perfect world, it appears that it can only really exist in one state: the state in which the room is being displayed. But for good measure let's consider the idea that we may introduce some logic error at some point, and if we do that, we might want to have some sort of hint as to what happened. So let's consider this application to have two overall state possibilities: DisplayingRoom
, which needs to be aware of both the Door
and the Alarm
, and Failure
, which needs to have some sort of message to tell us how it got there.
Well, it feels like we've just described our Model
, on top of our possible door and alarm states. Let's add all of that into our Main.elm
file now:
type Model
= DisplayingRoom DoorState AlarmState
| Failure String
type DoorState
= Opened
| Closed
| Locked
type AlarmState
= Armed
| Disarmed
| Triggered
This is how you create a custom type
in Elm, and all of the above are examples of custom types.
Coding our update
function
At this stage, since we did all of our planning up-front, we also have all of the information that we need in order to code our update
function. Update needs to take two things: a message
(which is a description of the transition that needs to happen), and the model
(which is the model before the update is applied), and it will return a new model.
Our transitions then, will take the form of messages
, and again thanks to our up-front thinking about our problem, we already know what they're going to be, so let's add them into our Main.elm
file now:
type Msg
= Open
| Close
| Lock
| Unlock
| Arm
| Disarm
(If you're wondering why we don't have a Trigger
message, it's because we don't have any way in our specification to manually trigger the alarm on its own!)
It's weird that we haven't seen anything yet though huh? Well, at first it is. In my experience sorting out your data before you sort out how things look works pretty nicely.
Right, back to work! Our update
function is going to take a message
and a model
, and it's going to return a new model
. We're going to start by checking what the state of the application (i.e. the value of the Model
) is:
update msg model =
case model of
DisplayingRoom doorState alarmState ->
model
Failure errorMessage ->
model
If we're in a Failure state, the update
function isn't going to have any way to recover, so it can just return the model
, and that's us done with Failure
in our update
function.
So what do we know about our possible state combinations when we're displaying our room? We know that we have the most constraints when our door is open - in fact, we only have one possible transition from that state, since we can't interact with our alarm until the door is closed. Let's add that in now:
update : Msg -> Model -> Model
update msg model =
case model of
DisplayingRoom doorState alarmState ->
case doorState of
Opened ->
case msg of
Close ->
DisplayingRoom Closed alarmState
_ ->
Failure "unexpected message received while door was in Opened state"
Failure _ ->
model
Now we're handling our Close
message when the door is in the Opened
state, and we're saying that in that scenario, the model we want to return is DisplayingRoom Closed alarmState
- alarmState
being the name of the variable that is holding the state that the alarm was in when it was passed into the model; this ensures that it is passed through to the new model without being altered when the door goes from Opened
to Closed
.
We're also saying that for any other message that is received when the door is in this state, we want to put our application into our Failure
state, with a message saying how it got there. We will be coding the view in such a way that no other messages should be possible here, but at least now if we make a mistake, we'll be have an easy way to give ourselves that information.
Next, let's handle our Closed
door state. In this case, we know that if the door is closed, and we receive an Open
message, then we need to care about the state of the alarm, and we also know that when the door is closed it's possible for us to manually change the state of the alarm too. So let's add all of that in:
Closed ->
case msg of
Open ->
case alarmState of
Armed ->
DisplayingRoom Opened Triggered
_ ->
DisplayingRoom Opened alarmState
Lock ->
DisplayingRoom Locked alarmState
Arm ->
DisplayingRoom Closed Armed
Disarm ->
DisplayingRoom Closed Disarmed
_ ->
Failure "unexpected message received while door was in Closed state"
Okay, so now if our door is opened whilst our alarm is armed, it will trigger the alarm. And we're handling the messages associated with the alarm now too. So all that's left to add is any messages that are possible whilst our door is locked
:
Locked ->
case msg of
Unlock ->
DisplayingRoom Closed alarmState
Arm ->
DisplayingRoom Locked Armed
Disarm ->
DisplayingRoom Locked Disarmed
_ ->
Failure "unexpected message received while door was in Locked state"
That's it for our update
functionality! So now we've added in everything that describes how our data can be represented, and all the ways that can change, it's time to add some view
code so we can see if it all works.
Coding our view
function
As I mentioned earlier, there's an HTML
library for Elm which is what allows us to write HTML within our Elm code. Import that now by adding
import Html exposing (..)
beneath your module declaration.
Let's start with the simplest part; our failure
function. We know we get a message from the model in this state, so let's create a function to display it:
failure message =
div []
[ p [] [ text message ] ]
Next, let's create our Door
. We know it needs to have two possible messages when it's closed, but only one message if it's either opened or locked. It would be easy to make a single function to represent all of these different states based on what's passed in, but frankly it's not a lot of code to create them all separately, and who knows how much further they'll diverge as my high-tech door-and-alarm application expands - let's not prematurely optimise ourselves into a corner. Have a look at my door module on Github, and you'll see we have all the door functionality that we need in order to meet our acceptance criteria here.
Now we're going to do the same thing for our Alarm
, which you can also view on Github. You'll note that in this case, we're saying that Alarm needs to know not only the message
that it has the ability to send (remember our alarm only ever has the ability to send one message depending on what state our application is in - it can never be in a state where we can arm
and disarm
it at the same time), but also it needs to know if any action is allowed. This gives us the ability to disable any alarm messages from being sent, but crucially the Alarm
module itself doesn't need to receive any messages from anywhere to make this happen. We'll handle that when we wire together our view
function in Main.elm
, which we'll do right now.
Remember to import
your Door
and Alarm
modules into your Main.elm
too. If you have them set up the same way as I do (i.e. View/Door.elm
and View/Alarm.elm
, and with each of those exposing a separate function depending on the door/alarm state), you can import those as follows:
import View.Alarm as Alarm exposing (armedAlarm, disarmedAlarm, triggeredAlarm)
import View.Door as Door exposing (closedDoor, lockedDoor, openDoor)
We want to display our door
and our alarm
, and we want to make sure they're displayed in the correct state depending on the state of our application. We know now what state our application is in to begin with, and what states it can possibly be in thereafter, and we've built the modules that will allow us to display what we need, so we just need to add the bit in the middle! Our view
function in Main.elm
then, should look like this:
view : Model -> Html Msg
view model =
case model of
Failure message ->
failure message
DisplayingRoom doorState alarmState ->
div
[]
[ div
[ class "doorPanel" ]
[ case doorState of
Opened ->
openDoor Close
Closed ->
closedDoor Open Lock
Locked ->
lockedDoor Unlock
]
, div
[ class "alarmPanel " ]
[ case alarmState of
Armed ->
armedAlarm Disarm (doorState /= Opened)
Disarmed ->
disarmedAlarm Arm (doorState /= Opened)
Triggered ->
triggeredAlarm Disarm (doorState /= Opened)
]
]
Here you can see we're checking the doorState
that came out of our model
, and calling the appropriate function depending on the state of the door, along with the appropriate message(s). We're doing the same thing with alarmState
, and into each of our alarm functions, we're passing the correct message to match up to our desired behaviour within the function, and we're also passing in the result of a check on doorState /= Opened
, since we told our alarm
functions to expect a boolean to state whether any action is allowed or not.
Wiring up the runtime
We need Elm's core Browser
module in order to make this do anything in the runtime. Add import Browser
to the top of your Main.elm
file, below your module declaration. (Note: Ordinarily you'd need to install
new packages before they could be imported, but Browser
was installed automatically when you ran elm init
)
Browser
has a few functions, but the one that we need here is Browser.sandbox. Sandbox allows you to create an application that uses the Elm architecture, but that doesn't talk to the "outside world" (i.e. any external APIs or JavaScript). It needs to take a record
that has three fields: init
, update
, and view
. We already have update
and view
functions, but the init
is going to describe the application's initial model - let's say that we're going to be displaying the room, with the door closed, and the alarm armed:
initialModel : Model
initialModel = DisplayingRoom Closed Armed
main : Program () Model Msg
main =
Browser.sandbox
{ init = initialModel
, view = view
, update = update
}
Running our application
Okay, so that's everything we need to do - in your terminal, run elm make src/Main.elm
. This will give you an index.html
file, which you can open in your browser of choice.
It just...worked?
It feels like there should be more to do really, doesn't it? It can be a bit odd at first, if you're used to web development being a cycle of "see how it looks -> play around with the code -> see how it looks now", but I've found working from the model
downwards rather than the view
upwards makes a lot of sense, and keeps things nice and clean.
I hope this is useful to someone!
Top comments (21)
Great post! So after this exercise, do you think making
Model
into a pure Custom Type is the way to go for larger projects? Or is this only suitable for small demos? (As opposed to the "traditional" ElmModel
, which is a record made up of many types and custom types.)Slight update: After adding navigation into another application, which meant needing to add a navigation key, I've ended up re-evaluating this at the moment. Although all I've done is added any globally-available info to the
model
, and kept thestate
the way I described earlier:And it's still the
state
that drives the UI itself:I do like this better. Somehow it feels a little "forced" to make the model as one giant custom type. With this type of edit, you can still model your door/alarm states elegantly and completely with a big custom type, and get all the benefits of that. But other stuff in the model (like navigation key or login status) that doesn't have anything to do with the door state can live separately, as a different piece of the model record.
Thanks! <3
I'm by no means an expert in Elm, so it's difficult for me to say for certain (although I'm currently working on a larger Elm application and I'm sure I'll come out of that with more thoughts!), but my current thought on this is that yes, I do prefer the idea of the
Model
being a Custom Type as opposed to a record. The main thing I kept coming up against when having the model be a record was that it seemed like every separate view ended up having information (or at least theoretical access to information) that it just didn't need - it could very well be that I wasn't organising my models very well, but since making this change in my own code things have felt easier to deal with and reason about.Again, my thoughts on this in future might change, but as of right now (which is after all when you're asking :D ) I think that this approach is helpful for reasoning about the application itself - if your Model is always describing the state of your application, then it feels likely that this will make life easier for anyone maintaining your application in the future - something I think we should all be careful to consider.
Also, looking at it more carefully... how is the top-level
DisplayingRoom
type defined? I'm sure that's a ridiculously basic question...Ah I think my wording has probably confused matters -
Model
is the custom type,DisplayingRoom
andFailure
are values that custom type can have. (These are known astype variants
)DoorState
andAlarmState
are also custom types.This might help clarify things a little:
Consider
Bool
- that is atype
that can have a value of eitherTrue
orFalse
, and would be represented (and I imagine probably actually is represented in the source code!) as:Does that answer your question? :)
Ooohh yes, that makes perfect sense! A custom type can have any... custom... values you make up, they don't have to be defined separately anywhere else. I get it now. My mental block was that I was still thinking of the
Model
type as a record, just because it was named Model. LOL.I guess I'm starting to see why "custom types" is a better name than "union types" (what they used to be in 0.18). Thanks for the update to 0.19!
Glad that helped! I can totally see how that caused confusion.
And yeah, I think the change of naming convention from
union
tocustom
is a big positive.Custom type
is a phrase that can be easily understood without even having any real understanding of the language at all! :)Interestingly I'd specifically been thinking a lot about modeling room states while wiring up a little home automation app. And I'd been considering Elm for the interface but needed a bit of help thinking through it. Soooo thanks. 😄
Hah, glad to be of service. Hope this ends up being useful for you then - keep me posted!
You should be to syntax highlight the source code like this:
Ah, I didn't realise this at all. Thanks for the tip!
Hi Nimmo,
We are now more than a year further of your original post. Do you still use the same structure? Or did you find another way to organize (after gaining more experience)?
Strange...it says I posted this in dec 18 while we are dec 19
Hey, yeah I'm still doing this (i.e., this way: dev.to/nimmo/comment/6i4n ), and have been full-time in production for months now. It's really nice!
I have an overall model in Main which is a record, that has a
state
, and my states inMain
tend to be things likeViewingPageX PageX.Model | ViewingPageY PageY.Model
etc. etc., and thenPageX.Model
andPageY.Model
would either just be a custom type that defined the states of their own pages, or they might also be a record if there's some info that needs to be available in every state (like, for example, an environment definition or something).Does that help? :)
Thanks for your feedback. This certainly helps. I like well structured code and like to learn from people more experienced with elm. Tutorials only cover small stuff...
Also I think the
Dec 18
on the comment is because it is the 18th of December, not December 2018! :DOf course, stupid me...
Not at all! Incredibly easy mistake to have made, just happened to be literally the only day of the year that it would have happened. :D
Nice model. It even avoids a problem found in some real-life doors: 1. open the the door, 2. toggle the deadbolt lock, 3. close the door, 4. "error: cannot close locked door"
Ha, yes! That's a perfect example of a state that our application could get into if we hadn't thought about the potential transitions up-front.