DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

Nimmo
Nimmo

Posted on • Updated on

The basic Elm example that I wish I'd had

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 (..)
Enter fullscreen mode Exit fullscreen mode

Β 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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

(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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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 ] ]
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
                    ]
                ]
Enter fullscreen mode Exit fullscreen mode

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
        }
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
kwitgit profile image
kwitgit

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" Elm Model, which is a record made up of many types and custom types.)

Collapse
 
nimmo profile image
Nimmo • Edited on

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 the state the way I described earlier:

-- MODEL


type alias Model =
    { key : Nav.Key
    , state : State
    }


type State
    = ViewSignUp SignUpData
    | ViewLogin LoginData
    | ViewPasswordReset PasswordResetData
    | Loading

And it's still the state that drives the UI itself:

-- VIEW


view : Model -> Document Msg
view model =
    case model.state of
        Loading ->
            { title = "Some app"
            , body = [ loadingView ]
            }

        ViewSignUp data ->
            let
                signUpView =
                    Html.map (\x -> SignUpMsg x) <| SignUp.view data
            in
            { title = "Sign up to Some app"
            , body = [ signUpView ]
            }

        ViewLogin data ->
            let
                loginView =
                    Html.map (\x -> LoginMsg x) <| Login.view data
            in
            { title = "Log in to Some app"
            , body = [ loginView ]
            }

        ViewPasswordReset data ->
            let
                forgottenPasswordView =
                    Html.map (\x -> PasswordResetMsg x) <| PasswordReset.view data
            in
            { title = "Request a password reset"
            , body = [ forgottenPasswordView ]
            }

Collapse
 
kwitgit profile image
kwitgit

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.

Collapse
 
nimmo profile image
Nimmo • Edited on

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.

Collapse
 
kwitgit profile image
kwitgit

Also, looking at it more carefully... how is the top-level DisplayingRoom type defined? I'm sure that's a ridiculously basic question...

Collapse
 
nimmo profile image
Nimmo • Edited on

Ah I think my wording has probably confused matters - Model is the custom type, DisplayingRoom and Failure are values that custom type can have. (These are known as type variants)

DoorState and AlarmState are also custom types.

This might help clarify things a little:

Consider Bool - that is a type that can have a value of either True or False, and would be represented (and I imagine probably actually is represented in the source code!) as:

type Bool 
  = True
  | False

Does that answer your question? :)

Thread Thread
 
kwitgit profile image
kwitgit

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!

Thread Thread
 
nimmo profile image
Nimmo

Glad that helped! I can totally see how that caused confusion.

And yeah, I think the change of naming convention from union to custom 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! :)

Collapse
 
ben profile image
Ben Halpern

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. πŸ˜„

Collapse
 
nimmo profile image
Nimmo

Hah, glad to be of service. Hope this ends up being useful for you then - keep me posted!

Collapse
 
joshcheek profile image
Josh Cheek

You should be to syntax highlight the source code like this:

source code that gets syntax highlighted

update msg model =
  case model of
    ViewRoom doorState alarmState ->
Collapse
 
nimmo profile image
Nimmo

Ah, I didn't realise this at all. Thanks for the tip!

Collapse
 
pasdut profile image
Pascal

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

Collapse
 
pasdut profile image
Pascal

Strange...it says I posted this in dec 18 while we are dec 19

Collapse
 
nimmo profile image
Nimmo

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 in Main tend to be things like ViewingPageX PageX.Model | ViewingPageY PageY.Model etc. etc., and then PageX.Model and PageY.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? :)

Thread Thread
 
pasdut profile image
Pascal

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

Collapse
 
nimmo profile image
Nimmo

Also I think the Dec 18 on the comment is because it is the 18th of December, not December 2018! :D

Thread Thread
 
pasdut profile image
Pascal

Of course, stupid me...

Thread Thread
 
nimmo profile image
Nimmo

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

Collapse
 
1hko profile image
1hko

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"

Collapse
 
nimmo profile image
Nimmo

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.

Classic DEV post:

CLI tools