Thinking the Elm way

A few months ago, I was introduced to a new way to write JavaScript. It’s name was Elm and its promise is to

Generate JavaScript with great performance and no runtime exceptions.

I was particularly intrigued by the latter part of the promise which prompted me to dip my toe in the water. Fast forward to today, I can safely say that Elm has fulfilled the promise it made.

Thinking back to my journey learning Elm, the most difficult part of getting comfortable with writing it was forcing myself to think the Elm way. I find that there is an Elm way of doing things, and trying to shoehorn concepts from other programming languages and frameworks into Elm will make things more difficult.

This blog post is primarily aimed at people who were in the same stage as I was a few months ago. It doesn’t aim to teach you how to write Elm code (the guide does a great job of that).

However, if you’re intrigued by Elm — maybe you’ve written a few lines, or even a small application or two — and you want to dip your toes further into the language; I aim to try and help you avoid some pitfalls I fell into when writing more complex Elm applications and understand what Elm really brings to the table.

A practical approach

I try to approach learning new tools by doing something practical with them. A small version of something I would do in a professional setting so I can get a feel for how things work. A great example of this in the JavaScript world is making HTTP requests using JavaScript to dynamically load data onto the page without refreshing the browser.

Here is a simple ES6 example that does just that. One of the things I enjoy about JavaScript, and especially with the more modern ES6 syntax and the fetch API, is the ability to prototype concepts like this very quickly.

The same problem solved using Elm is a lot different. I’m assuming the reader of this post understands JavaScript well and is comfortable with it, so I won’t walk through what the JavaScript code is doing. I’m also assuming that if you’re just getting started with Elm, the same familiarity with vanilla JavaScript may not be there so let’s walk through the code example to try and understand what it’s doing.

Imports

module GitHubUser exposing (..)import Html exposing (Html, div, text, program, button, input)
import Html.Attributes exposing (type_, placeholder)
import Html.Events exposing (onClick, onInput)
import Http
import Json.Decode as Decode

We’re importing a bunch of modules in Elm. If you’ve used any form of modern programming language before, you should recognise what this is trying to do. The interesting part of this is the explicit exposing keyword, which has multiple meanings, depending on if you start the line with import or module.

In the example above, import Html.Events exposing (onClick, onInput) roughly translates to “allow me to use onClick and onInput without referencing the full namespaced variants e.g. Html.Events.onClick”.

Conversely, the module GithubUser exposing (...) declaration exposes the current module and all functions within it. This means that any module that imports the GithubUser module can use any function defined within it. If you wish, you may only expose a part of your module (using a comma separated list of functions) limiting the API to only the functions you want to call externally.

Model

The model in Elm is a way of representing the problem you’re trying to solve in an expressive and, more important, explicit manner. Solving the Boolean Identity Crisis by Jeremy Fairbank is an excellent talk that goes into detail about how some of the tools we use make use model data in a manner that doesn’t fully represent the problem.

In my example, I want to store the data about the form and the data returned from the API separately

type alias FormModel =
{ username : String
, errors : List String
}
type alias GitHubModel =
{ name : String }
type alias Model =
{ form : FormModel
, gitHubData : GitHubModel
}

I created two type aliases around the records that represent my model. FormModel represents the form, and GitHubModel represents the data I’ve retrieved from the GitHub API. The Model wraps these two up in a nested record format.

Anytime I ask for Model, I’m asking for

{ 
form : { username : String,
errors : List String },
gitHubData : { name: String }
}

The alias is much nicer to use than sprinkling a bunch of verbose definitions everywhere.

Initialise

Now that I have a definition of my model, the next step is to initialise the model with default values.

I create two functions that help build my initial form.

initialFormUsername : String
initialFormUsername =
""
initialForm : FormModel
initialForm =
{ username = initialFormUsername, errors = [] }
model : Model
model =
{ form =
{ initialForm | errors = validateForm initialForm initialFormUsername }
, gitHubData =
{ name = initialFormUsername
}
}

If you’ve not seen a function before in Elm, read this.

initialFormUsername is a blank string. I pulled it out into a function because it’s used in a few different places. initialForm builds out the initial form record, defaulting the username to initialFormUsername and the errors to an empty list.

Finally the model definition validates the initialForm.errors field and assigns the returned value to form.errors. This allows the initial errors array to be pre-validated upon initialisation.

Messages

Messages are a way of telling the Elm Runtime something occurred in your application and you would like to perform something (or nothing) based on this message.

type Msg
= RequestGitHubData
| ProcessGitHubHttpRequest (Result Http.Error Model)
| InputUserName String

These are the things that can happen in my Elm application. Elm union types are perfect for messages because you can model the actions in a more complex manner than simpler types. Because these are strictly typed, the compiler can enforce that only these messages can occur within my application, and furthermore it can pattern match to ensure that I’ve covered all the different branches of Msg wherever I use it.

Update

The update function in Elm is what called every time a Msg in my application is triggered. When a Message is triggered, I’m saying “something has happened, if you care about this something, now is the time to take action”.

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
RequestGitHubData ->
( model, requestGitHubData model.form )
ProcessGitHubHttpRequest (Ok responseModel) ->
( responseModel, Cmd.none )
ProcessGitHubHttpRequest (Err _) ->
( model, Cmd.none )
InputUserName username ->
( { model | form = { username = username, errors = validateForm model.form username } }, Cmd.none )

In my update method, I’m destructuring the msg variable and, for each type of message, I’m performing an action. For example, if a msg is RequestGitHubData, I perform the requestGitHubDatacommand.

View

The view, in this instance, renders the HTML. The view where a user can invoke your Elm messages, for example: button [ onClick RequestGitHubData ] which invokes RequestGitHubData every time the button is clocked.

formValidationView : FormModel -> Html Msg
formValidationView formModel =
div [] [ text (String.join "" formModel.errors) ]
githubButtonView : FormModel -> Html Msg
githubButtonView formModel =
if List.isEmpty formModel.errors then
button [ onClick RequestGitHubData ]
[ text "Gather data" ]
else
text ""
formView : FormModel -> Html Msg
formView formModel =
div []
[ input [ type_ "text", placeholder "Username", onInput InputUserName ] []
, githubButtonView formModel
]
dataView : GitHubModel -> Html Msg
dataView model =
div []
[ text ("Username is " ++ model.name)
]
view : Model -> Html Msg
view model =
if model.gitHubData.name == "" then
div [] [ formView model.form, formValidationView model.form ]
else
dataView model.gitHubData

The Elm way

Defining the Elm way is quite difficult to do concretely, but if you think about the differences between the vanilla JavaScript and Elm examples, there are a few differences you can notice:

The Elm code is more verbose than the JavaScript version. Some of this can be explained by Elm’s syntax, e.g. adding type annotations to the functions.

However, one of the key reasons for the verbosity is the following:

type alias FormModel =
{ username : String
, errors : List String
}

I’m being very explicit in what FormModel is. The username property can only be a String. The simple concept of telling the language what type a property is allows the compiler to verify the integrity of the property during the duration of the program’s execution, and reduce the chances of runtime errors occurring.

This theme of being explicit is continued with the very powerful but simple union type concept in Elm which allows you to define the multiple states for whatever logic you are currently modelling in an explicitand descriptive manner.

An example in the Elm guide is filtering a todo list:

type Visibility = All | Active | Completed

We’re telling our application that the todo list filter can only ever be in one of these three states. If you try to check for a ActiveOrCompleted state which doesn’t exist, the code won’t compile. Conversely, if you add ActiveOrCompleted and don’t cover that branch everywhere that you use Visibility, the code won’t compile.

This truly powerful way of explicitly defining the states of complex data in your application is one of the reasons I truly love Elm.

Ask yourself this: how would you store similar state in vanilla JavaScript? if model.state == "Visible"?

That’ll work, but there are disadvantages to this approach. While it is possible to validate the state in a similar manner, this will have to be stored at runtime. If the state isn’t the correct string e.g. "visible"instead of "Visible", then what do you do?

Furthermore, what we’re saying is that a model’s state, which is explicit and has only three possible values, can be represented by a String which has a lot more than three possible values.

A String is not created for representing the state of a todo list, however, JavaScript doesn’t give us the tools to accurately represent this or create our own, so we shoehorn existing concepts which don’t fit quite well.

Finally, while you can certainly check against that the state being invalid at runtime, what’s the right thing to do when the state is invalid? Error message? Throw an exception? All of these questions we need to ask, and answer when writing vanilla JavaScript (or risk runtime errors).

Elm does not have this problem. The explicitness and verbosity of Elm means that a model simply cannot be in an invalid state. This is enforced by the compiler. This is the true beauty of Elm.

To me, thinking the Elm way is synonymous with thinking about modelling complex states. You don’t have to worry about invalid states, or how to handle them, you simply tell your program what the correct states of its data structures can be and it goes ahead and generates JavaScript that verifies that correctness.

The source code for the examples in the post can be found here.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store