Web development using the Elm programming language
The front end side of web development has been rapidly changing for years now with tons of frameworks, tools, and tiny npm libraries. A lot of this has been due to limitations in the JavaScript programming language and its standard API present in web browsers, but plenty of frameworks exist now for implementing a rich web application using an MVC or MVC-like architecture. The rise of single page applications where the entire site is dynamically loaded and modified through JavaScript has made the need for stricter architectural patterns a modern requirement of web development.
At the end of 2016, the hottest JavaScript web frameworks appear to be React and Angular, though they are not the only ones out there (for an interesting comparison of various JS and web frameworks, see TodoMVC). As standard JavaScript (EcmaScript) development picked up the pace in recent years, many ideas that were popularized in libraries and frameworks are finding standardized APIs and improved syntax, but as any seasoned web developer knows, relying on current web standards isn’t always that pragmatic in practice due to the various standards-incompliant browsers in the wild along with older versions of browsers still being supported.
During recent years, there have been several attempts at writing new programming languages for the web including Dart, CoffeeScript, TypeScript, Flow, and Ceylon, and even some non-web languages being ported to the web including Scala.js, Clojurescript, and GopherJS (Go). However, one new language stands amongst them all as a language without the baggage of JavaScript while still being easy enough to learn that a JavaScript developer can slowly adopt it into their own projects. That language, naturally, is Elm.
Why Elm?
Why exactly should you care about yet another programming language for the web when everyone already knows JavaScript? Why should you bother learning yet another framework and set of tools to develop web applications? Elm offers several killer features, two of which you’ll never find in JavaScript: the inability to cause runtime errors and a time-traveling debugger. Just these features alone can help alleviate tons of pain from typical JS development. There are several other features that are worth mentioning including:
- Ease of refactoring.
- Easy to understand standard architecture for writing code.
- Use of stateless functions to reduce state-caused bugs.
- Reliable runtime environment.
- Better syntax for performing callbacks/promises.
- Friendly compiler error messages.
- Automatic semantic versioning enforcement for libraries.
- Nice online IDEs (and offline IDE support of course).
- Easy to learn syntax.
- Interoperates with existing JavaScript code with safety barriers.
Many of the above points are discussed in this article. The Elm documention provides a great getting started guide that teaches you most of what you need to know to get started using Elm in real applications. In this article, we’ll go over a simple Elm application that uses our sample blog microservice developed previously. If you’re purely a front end developer, don’t worry; we’ll go over the REST API exposed by that service.
Developing an example Elm app
In previous posts, we’ve developed a rather simple blog REST API. We have three REST calls available:
GET /api/blog/:id
– gets a blog post by ID.POST /api/blog
– adds a new blog post; response gives us the new ID.PUT /api/blog/:id
– updates a blog post by ID.
The data model we use looks like this:
{
"title": "Some Title",
"author": "Some Author",
"body": "Post contents go here. No specific formatting was specified."
}
Simple enough, right? We’re going to implement a simple web app for this API. The code for these examples are in this GitHub project. We’ll briefly go over the Elm Architecture as it is already explained rather well in the getting started guide, then we’ll implement the various functionality for our app.
The Elm Architecture
Elm applications tend to follow a very standard architecture consisting of a data model, a set of message types, an update function which receives messages and the current model and returns an updated model, and a view function which receives the current model and returns HTML with attached event listeners which send messages back to the update function. More advanced Elm applications also include an initialization function for the model, additions to the update function to return commands for performing asynchronous operations (which themselves are hooked back into your code via messages again), and subscriptions which are used for receiving updates and responding with messages.
Essentially, this architecture works by beginning with an initial model. This
model is provided to the view function which transforms that model into HTML.
In that HTML transformation are standard DOM event handlers, but instead of
attaching functions to DOM events, we instead provide a message type for that
event. When the event fires (e.g., an input
event in a text <input>
), Elm
creates a message using the type provided and any related information to that
event (e.g., an input
event will provide the string value of the <input>
).
This message is passed to the update function along with the current model,
and that function returns an updated model along with a command to execute in
response. If a command is provided, this command completes by sending a message
which gets fed back into the update function again. This completes the standard
lifecycle of an Elm application.
A bit abstract, isn’t it? Don’t worry, it’ll become a bit clearer while we work through an example application.
Model
Generally, we can always start development by creating a model to represent the state of our application. For our purposes, we already saw most of the model above as defined by the REST API. In fact, all four pieces of data we’ll need were specified there, so let’s create our model:
type alias BlogPost =
{ id : String
, title : String
, author : String
, body : String
}
The syntax is pretty straightforward. In Elm, a type alias
is what it sounds
like: an alias for a type. In this case, a BlogPost
will be an alias for a
generic Elm record (similar to a JavaScript object) with four fields, all of
which are String
types. Creating a type alias
also creates a constructor
for that record which takes all arguments in order. For example, we can create
a BlogPost
record:
post = BlogPost "1" "Hello, world!" "John Doe" "Test post contents."
Update
Next, we’ll work on our update function. We start by defining a message type union along with some message types. For simplicity, we’ll only define messages related to getting a blog post for now.
type Msg
= ChangeId String
| GetPost
| LoadPost (Result Http.Error BlogPost)
Let’s go over each message type in detail. First, the ChangeId String
message
will be used as an onInput
event handler for an input field to look up a blog
post by ID. We’ll use this message to update the model only; we don’t want to
attempt a post lookup for every character in the ID!
Next, the GetPost
message will be used as both an onClick
event handler for
the submit button along with an onSubmit
event handler for the form itself.
This message will indicate that we wish to perform a lookup for the post ID
stored in our model.
Finally, the LoadPost (Result Http.Error BlogPost)
message will be used to
indicate that our HTTP call has completed. Since an HTTP call can succeed or
fail, this uses a Result
type which can contain either an Http.Error
on
error or a BlogPost
on success. We’ll use this message to update our model
in response to the completion of the HTTP call.
Now that we have some message types and knowledge of what we wish to do with said messages, next we’ll implement an update function.
update : Msg -> BlogPost -> (BlogPost, Cmd Msg)
update msg post =
case msg of
ChangeId id ->
({ post | id = id }, Cmd.none)
GetPost ->
(post, getBlogPost post.id)
LoadPost (Ok loadedPost) ->
({ loadedPost | id = post.id }, Cmd.none)
LoadPost (Err _) ->
(post, Cmd.none)
The first line is a type signature specifying that update
is a function that
takes two parameters, a Msg
and a BlogPost
, and returns a tuple containing
a BlogPost
and a Cmd Msg
. We’ll come back to the Cmd Msg
part in just a
bit, but let’s take a closer look at this case msg of ...
expression.
Recall that our Msg
type is actually a type union of three different types.
This means that in order determine which type of message we’ve received in this
function, we need to perform what’s called pattern matching on the message.
In our first match, we check if the message is a ChangeId String
type where
the String
is given the variable name id
. Our reaction to a ChangeId
message is to change the ID of our model. As records are not mutable in Elm, we
must return a new record with an updated value. Elm provides a convenient syntax
to return a modified copy of an existing record: { post | id = id}
. Our
second return parameter, Cmd.none
, indicates that we do not wish to execute
any commands in response to this message. One possible use case for a command
here would be for implementing an autosuggest feature.
The second match checks for a message of type GetPost
. When this message is
received, we know we’d like to actually perform a GET
call to our REST API.
In response, we return an unmodified model along with a command to go get a
blog post with the id specified by the model, model.id
. We’ll come back to
the implementation of the getBlogPost
function in just a moment.
The last two matches are for LoadPost
messages, the first being the success
case and the second being the error case. In the error case, we don’t do
actually do anything, but a more robust application might attempt to retry the
API call or display an error message to the user. In the success case, we’ll
receive a BlogPost
model parsed from the REST API lookup, but we update the
ID of the post using the ID we had previously as our API doesn’t actually
return the ID of the post in its body. We return a no-op Cmd.none
as well
here.
Let’s dive further into our HTTP GET call.
baseUrl = "http://localhost:8080/api/blog/"
getBlogPost : String -> Cmd Msg
getBlogPost id =
let
url = baseUrl ++ id
request = Http.get url (blogPostDecoder id)
in
Http.send LoadPost request
Our getBlogPost
function takes one parameter, the ID of the blog post to look
up, and it returns a command to perform an HTTP GET call along with a message
to send on completion. Looking further, we use a let ... in ...
expression
which allows us to define local variables to that function. The ++
operator
is the string concatenation operator in Elm, so the url
variable is pretty
simple. The request
variable will be used by the Http.send
function to
transform a request into a command. We create the request using two parameters:
a URL to GET, and a JSON decoder to parse the response with. We’ll look at the
JSON decoder in more detail in just a moment. Finally, we use this request
along with the LoadPost
message type to create a command using Http.send
.
This command will be used by Elm to perform the actual HTTP request which
completes by sending a LoadPost
message.
Let’s look at the JSON decoder function:
blogPostDecoder : String -> Decoder BlogPost
blogPostDecoder id =
map3 (BlogPost id)
(field "title" string)
(field "author" string)
(field "body" string)
Note that we’ve cheated a bit with syntax here slightly by importing the
functions map3
, field
, and string
, along with the type Decoder
, all
from the Json.Decode
module. This saves us a bit of typing, but the above
could also be written as:
blogPostDecoder : String -> Json.Decode.Decoder BlogPost
blogPostDecoder id =
Json.Decode.map3 (BlogPost id)
(Json.Decode.field "title" Json.Decode.string)
(Json.Decode.field "author" Json.Decode.string)
(Json.Decode.field "body" Json.Decode.string)
This function creates a BlogPost
JSON decoder. Since we don’t receive an id
field in our REST API response, we fill that in ourselves by constructing a
partially applied function, (BlogPost id)
, which now only requires three
parameters to specify the remaining fields. Note that the JSON API provided by
the core Elm package is rather low level in order to provide type safety, but
there are other packages available to make this a bit simpler. In the case of
our decoder, we’re specifying that we expect a JSON object with three fields,
all of which are strings.
View
Finally, we need to implement a view to display our UI. In this example, we’ll
be using some wildcard imports for the various HTML functions for creating HTML
elements and attributes. I will note ahead of time that if an HTML element or
attribute name conflicts with an Elm keyword, then it’ll have an underscore
suffix. For example the type
attribute from HTML will be implemented as a
function called type_
in Elm.
view : BlogPost -> Html Msg
view post =
section []
[ article []
[ header []
[ h1 [] (formatTitle post) ]
, div [] [ post.body ]
]
, Html.form [ onSubmit GetPost ]
[ div []
[ label [ for "blog-id" ] [ text "id" ]
, input
[ id "blog-id"
, type_ "text"
, onInput ChangeId
, value post.id
, placeholder "12345678-1234-1234-1234-1234567890ab"
] []
, span []
[ button [ onClick GetPost ] [ text "get post" ] ]
]
]
]
formatTitle : BlogPost -> List (Html Msg)
formatTitle post =
if String.isEmpty post.title then
[ text "" ]
else
[ text <| post.title ++ " "
, small []
[ text <| "by " ++ post.author
]
]
Note that I’ve removed a bit of unnecessary wrapper HTML and classes that are
used in the GitHub project to provide some styling. One big thing to notice
here is that Elm does not use any sort of template language. Instead, Elm
creates HTML through standard Elm functions. This allows for certain
optimizations in displaying the resulting HTML as well as allowing various
page components to be refactored into functions instead. Based on our imports,
all the HTML element functions would otherwise be prefixed by Html.
, the
attributes would be prefixed by Html.Attributes.
, and the event handlers
would be prefixed by Html.Events.
. The text
function creates a text node
and is not an actual HTML element. The Html
module provides all the common
HTML5 elements and attributes, but custom element and attribute names can also
be created through the Html.node
and Html.Attributes.attribute
functions
respectively.
Besides all the actual markup, there are just a couple things to note in this
code. First, we’ve specified that any input
events on the ID text field
will send ChangeId
messages which we’ve implemented earlier in our update
function. On both the form and the button, we’ve specified that the GetPost
message should be sent for submit
and click
events respectively. Finally,
we fill in data from our model when present into these elements. We use a
fancier function for creating the blog post header so that it only display
when a title is present. The only unknown syntax here is the <|
operator
which means “put the rest of the arguments after this in parenthesis”. That is,
both of the following snippets are equivalent:
foo bar <| a b c d
foo bar (a b c d)
Putting it all together
There is a little bit of boilerplate not shown here that is required to finish
the file which includes specifying a main
function and a subscriptions
function. Both of these functions are written in the full example.
This full example also includes a form for creating and editing blog posts,
markdown parsing of the post body, and some styling using Bootstrap.
The GitHub project also includes instructions on running a local
copy of the blog microservice (requires Java 8 and Maven). This project works
using Elm 0.18 and only requires the Elm runtime.
Elm shows a lot of promise as a friendly and easy functional programming language for web development. End your JavaScript fatigue by trying out the Elm language and package ecosystem today! Elm is already used in production in several websites, and thanks to semantic versioning, can be safely used without worrying about backwards incompatible upgrades between minor versions. The best way to go about using Elm in production today is to gradually start using it with your existing JavaScript code.