Technology17 minute read

Make Your Web Front-end Reliable with Elm

If you’ve spent your fair share of time developing web front-ends, you know that no amount of libraries and plugins are sufficient enough to make the development experience pleasant. Unpredictable event chains, complex data binding, and lack of structured data modeling only makes things worse.

Elm, a programming language built for front-end development, cuts to the root of all these problems and solves them there.

In this post, Toptal Software Engineer Stanislav Davydov provides a detailed guide to Elm and shows us how The Elm Architecture solves some of the most fundamental challenges of front-end development.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

If you’ve spent your fair share of time developing web front-ends, you know that no amount of libraries and plugins are sufficient enough to make the development experience pleasant. Unpredictable event chains, complex data binding, and lack of structured data modeling only makes things worse.

Elm, a programming language built for front-end development, cuts to the root of all these problems and solves them there.

In this post, Toptal Software Engineer Stanislav Davydov provides a detailed guide to Elm and shows us how The Elm Architecture solves some of the most fundamental challenges of front-end development.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
Stas Davydov
Verified Expert in Engineering

Stanislav’s 18+ years in organizations of all sizes have focused on eCommerce, complex web services, friendly UI, and data mining/scraping.

PREVIOUSLY AT

Emergn
Share

How many times have you tried to debug your web front-end and found yourself tangled in code dealing with complex chains of events?

Did you ever try to refactor code for a UI dealing with a lot of components built with jQuery, Backbone.js, or other popular JavaScript frameworks?

One of the most painful things about these scenarios is trying to follow the multiple indeterminate sequences of events and anticipating and fixing all these behaviors. Simply a nightmare!

I’ve always looked for ways to escape this hellish aspect of web front-end development. Backbone.js worked well for me in this regard by giving web front-ends the structure they’ve been missing for a long time. But given the verbosity it requires to do some of the most trivial things, it didn’t turn out to be much better.

Make Your Web Front-end Reliable with Elm

Then I met Elm.

Elm is a statically typed functional language based on the Haskell programming language, but with a simpler specification. The compiler (also built using Haskell) parses Elm code and compiles it into JavaScript.

Elm was originally built for front-end development, but software engineers have found ways to use it for the server-side programming as well.

This article provides an overview of how Elm can change the way we think of web front-end development and an introduction to the basics of this functional programming language. In this tutorial, we’ll develop a simple shopping cart-like application using Elm.

Why Elm?

Elm promises a lot of advantages, most of which are extremely useful in achieving a clean web front-end architecture. Elm offers better HTML rendering performance advantages over other popular frameworks (even React.js). Moreover, Elm allows developers to write code, which in practice, do not produce most runtime exceptions that plague dynamically typed languages like JavaScript.

The compiler infers types automatically and emits friendly errors, making the developer aware of any potential issue before runtime.

NoRedInk has 36,000 lines of Elm and, after more than a year in production, still has not produced a single runtime exception. [Source]

You do not need to convert your entire existing JavaScript application just so that you can try Elm out. Through its superb interoperability with JavaScript, you can even take just a small part of your existing application, and rewrite it in Elm.

Elm also has excellent documentation that not only gives you a thorough description of what it has to offer, but also gives you a proper guide to building web front-end following The Elm Architecture - something that is great for modularity, code reuse, and testing.

Let’s Make a Simple Cart

Let us begin with a very short snippet of Elm code:

import List exposing (..) 
 
cart = []
 
item product quantity = { product = product, qty = quantity }
 
product name price = { name = name, price = price }
 
add cart product = 
  if isEmpty (filter (\item -> item.product == product) cart) 
    then append cart [item product 1] 
    else cart 
 
subtotal cart = -- we want to calculate cart subtotal 
  sum (map (\item -> item.product.price * toFloat item.qty) cart)

Any text preceded by -- is a comment in Elm.

Here we are defining a cart as a list of items, where every item is a record with two values (the product that it corresponds to and the quantity). Each product is a record with a name and a price.

Adding a product to the cart involves checking if the item already exists in the cart.

If it does, we do nothing; otherwise, we add the product to the cart as a new item. We check if the product already exists in the cart by filtering the list, matching each item with the product, and checking if the resulting filtered list is empty.

To calculate the subtotal, we iterate over the items in the cart, finding the corresponding product quantity and price, and summing it all up.

This is as minimalistic as a cart and its related functions can get. We will begin with this code, and improve it step-by-step making it a complete web component, or a program in Elm’s terms.

Let us start by adding types to the various identifiers in our program. Elm is capable of inferring types by itself but to make the most out of Elm and its compiler, it’s recommended that the types are indicated explicitly.

module Cart1 exposing 
  ( Cart, Item,  Product 
  , add, subtotal 
  , itemSubtotal 
  ) -- This is module and its API definition 
 
{-| We build an easy shopping cart. 
@docs Cart, Item, Product, add, subtotal, itemSubtotal 
-} 
 
import List exposing (..) -- we need list manipulation functions 
 
 
{-| Cart is a list of items. -} 
type alias Cart = List Item 
 
{-| Item is a record of product and quantity. -} 
type alias Item = { product : Product, qty : Int } 
 
{-| Product is a record with name and price -} 
type alias Product = { name : String, price : Float } 
 
 
{-| We want to add stuff to a cart. 
    This is a function definition, it takes a cart, a product to add and returns new cart -} 
add : Cart -> Product -> Cart 
 
{-| This is an implementation of the 'add' function. 
    Just append product item to the cart if there is no such product in the cart listed. 
    Do nothing if the product exists. -} 
add cart product = 
  if isEmpty (filter (\item -> item.product == product) cart) 
    then append cart [Item product 1] 
    else cart 
 
 
{-| I need to calculate cart subtotal. 
    The function takes a cart and returns float. -} 
subtotal : Cart -> Float 
 
{-| It's easy -- just sum subtotal of all items. -} 
subtotal cart = sum (map itemSubtotal cart) 
 
 
{-| Item subtotal takes item and return the subtotal float. -} 
itemSubtotal : Item -> Float 
 
{-| Subtotal is based on product's price and quantity. -} 
itemSubtotal item = item.product.price * toFloat item.qty 

With type annotations, the compiler can now catch issues that would otherwise have resulted in runtime exceptions.

However, Elm doesn’t stop there. The Elm Architecture guides developers through a simple pattern for structuring their web front-ends, and it does so through concepts that most developers are already familiar with:

  • Model: Models hold the state of the program.
  • View: View is a visual representation of the state.
  • Update: Update is a way to change the state.

If you think of the part of your code dealing with updates as the controller, then you have something very similar to good old Model-View-Controller (MVC) paradigm.

Since Elm is a pure functional programming language, all data are immutable, which means that the model cannot be changed. Instead, we can create a new model based on the previous one, which we do through update functions.

Why is that so great?

With immutable data, functions can no longer have side effects. This opens up a world of possibilities, including Elm’s time traveling debugger, which we will discuss shortly.

The views are rendered every time a change in the model requires a change in the view, and we will always have the same result for the same data in the model - in much the same way that a pure function always returns the same result for the same input arguments.

Start with the Main Function

Let’s go ahead and implement the HTML view for our cart application.

If you are familiar with React, this is something I am sure you will appreciate: Elm has a package just for defining HTML elements. This allows you to implement your view using Elm, without having to rely on external templating languages.

Wrappers for the HTML elements are available under the Html package:

import Html exposing (Html, button, table, caption, thead, tbody, tfoot, tr, td, th, text, section, p, h1)

All Elm programs start by executing the main function:

type alias Stock = List Product 
type alias Model = { cart : Cart, stock : Stock } 

main =
  Html.beginnerProgram
    { model = Model []
      [ Product "Bicycle" 100.50
      , Product "Rocket" 15.36
      , Product "Biscuit" 21.15
      ]
    , view = view
    , update = update
    }

Here, the main function initializes an Elm program with some models, a view, and an update function. We have defined a few kinds of products and their prices. For simplicity, we are assuming that we have an unlimited number of products.

A Simple Update Function

The update function is where our application comes to life.

It takes a message and updates the state based on the content of the message. We define it as a function that takes two parameters (a message and the current model) and returns a new model:

type Msg = Add Product 

update : Msg -> Model -> Model 
 
update msg model = 
  case msg of 
    Add product -> 
      { model | cart = add model.cart product }

For now, we are handling a single case when the message is Add product, where we call the add method on cart with the product.

The update function will grow as the complexity of the Elm program grows.

Implementing the View Function

Next, we define the view for our cart.

The view is a function that translates the model into its HTML representation. However, it is not just static HTML. The HTML generator is capable of emitting messages back to the application based on various user interactions and events.

view : Model -> Html Msg 
 
view model = 
  section [style [("margin", "10px")]] 
    [ stockView model.stock 
    , cartView model.cart 
    ] 

stockView : Stock -> Html Msg 
 
stockView stock = 
  table [] 
    [ caption [] [ h1 [] [ text "Stock" ] ] 
    , thead [] 
      [ tr [] 
        [ th [align "left", width 100] [ text "Name" ] 
        , th [align "right", width 100] [ text "Price" ] 
        , th [width 100] [] 
        ] 
      ] 
    , tbody [] (map stockProductView stock) 
    ] 

stockProductView : Product -> Html Msg 
 
stockProductView product = 
  tr [] 
    [ td [] [ text product.name ] 
    , td [align "right"] [ text ("\t$" ++ toString product.price) ] 
    , td [] [ button [ onClick (Add product) ] [ text "Add to Cart" ] ] 
    ] 

The Html package provides wrappers for all commonly used elements as functions with familiar names (e.g. the function section generates a <section> element).

The style function, part of the Html.Attributes package, generates an object that can be passed to the section function to set the style attribute on the resulting element.

It’s better to split the view into separate functions for better reusability.

To keep things simple, we have embedded CSS and some layout attributes directly into our view code. However, libraries exist that streamline the process of styling your HTML elements from Elm code.

Notice the button near the end of the snippet and how we have associated the message Add product to the click event of the button.

Elm takes care of generating all necessary code for binding a callback function with the actual event and the generating and calling the update function with relevant parameters.

Finally, we need to implement the last bit of our view:

cartView : Cart -> Html Msg 
 
cartView cart = 
  if isEmpty cart 
    then p [] [ text "Cart is empty" ] 
    else table [] 
      [ caption [] [ h1 [] [ text "Cart" ]] 
      , thead [] 
        [ tr [] 
          [ th [ align "left", width 100 ] [ text "Name" ] 
          , th [ align "right", width 100 ] [ text "Price" ] 
          , th [ align "center", width 50 ] [ text "Qty" ] 
          , th [ align "right", width 100 ] [ text "Subtotal" ] 
          ] 
        ] 
      , tbody [] (map cartProductView cart) 
      , tfoot [] 
        [ tr [style [("font-weight", "bold")]] 
          [ td [ align "right", colspan 4 ] [ text ("$" ++ toString (subtotal cart)) ] ] 
        ] 
      ] 

cartProductView : Item -> Html Msg 
 
cartProductView item = 
  tr [] 
    [ td [] [ text item.product.name ] 
    , td [ align "right" ] [ text ("$" ++ toString item.product.price) ] 
    , td [ align "center" ] [ text (toString item.qty) ] 
    , td [ align "right" ] [ text ("$" ++ toString (itemSubtotal item)) ] 
    ] 

Here we have defined the other part of our view where we render the contents of our cart. Although the view function doesn’t emit any message, it still needs to have the return type Html Msg to qualify as a view.

The view not only lists the contents of the cart but also computes and renders the subtotal based on the cart’s contents.

You can find the full code for this Elm program here.

If you were to run the Elm program now, you would see something like this:

How does it all work?

Our program starts with a fairly empty state from the main function - an empty cart with a few hard coded products.

Every time the “Add to Cart” button is clicked, a message is sent to the update function, which then updates the cart accordingly and creates a new model. Whenever the model is updated, the view functions are invoked by Elm to regenerate the HTML tree.

Since Elm uses a Virtual DOM approach, similar to that of React, changes to the UI are performed only when necessary, ensuring snappy performance.

Not Just a Type Checker

Elm is statically typed, but the compiler can check a lot more than just types.

Let’s make a change to our Msg type and see how the compiler reacts to that:

type Msg = Add Product | ChangeQty Product String 

We have defined another kind of message - something that would change the quantity of a product in the cart. However, attempting to run the program again without handling this message in the update function will emit the following error:

Toward a More Functional Cart

Note that in the previous section we used a string as the type for the quantity value. This is because the value will come from an <input> element which will be of type string.

Let’s add a new function changeQty to the Cart module. It’s always better to keep implementation inside the module to be able to change it later if required without changing the module API.

{-| Change quantity of the product in the cart. 
    Look at the result of the function. It uses Result type. 
    The Result type has two parameters: for bad and for good result. 
    So the result will be Error "msg" or a Cart with updated product quantity. -} 
changeQty : Cart -> Product -> Int -> Result String Cart 
 
{-| If the quantity parameter is zero the product will be removed completely from the cart. 
    If the quantity parameter is greater then zero the quantity of the product will be updated. 
    Otherwise (qty < 0) the error will be returned. 
-} 
changeQty cart product qty = 
  if qty == 0 then 
    Ok (filter (\item -> item.product /= product) cart) 
 
  else if qty > 0 then 
    Result.Ok (map (\item -> if item.product == product then { item | qty = qty } else item) cart) 
 
  else 
    Result.Err ("Wrong negative quantity used: " ++ (toString qty)) 

We shouldn’t make any assumptions about how the function will be used. We can be confident that the parameter qty will contain an Int but the value can be anything. We therefore check the value and report an error when it is invalid.

We also update our update function accordingly:

update msg model = 
  case msg of 
    Add product -> 
      { model | cart = add model.cart product } 
 
    ChangeQty product str -> 
      case toInt str of 
        Ok qty -> 
          case changeQty model.cart product qty of 
            Ok cart -> 
              { model | cart = cart } 
 
            Err msg -> 
              model -- do nothing, the wrong input 
 
        Err msg -> 
          model -- do nothing, the wrong quantity 

We convert the string quantity parameter from the message to a number before using it. In case the string holds an invalid number, we report it as an error.

Here, we keep the model unchanged when an error happens. Alternatively, we could just update the model in a way to report the error as a message in the view for the user to see:

type alias Model = { cart : Cart, stock : Stock, error : Maybe String } 
main = 
  Html.beginnerProgram 
    { model = Model [] -- empty cart 
      [ Product "Bicycle" 100.50 -- stock 
      , Product "Rocket" 15.36 
      , Product "Bisquit" 21.15 
      ] 
      Nothing -- error (no error at beginning) 
    , view = view 
    , update = update 
    } 
 
update msg model = 
  case msg of 
    Add product -> 
      { model | cart = add model.cart product } 
 
    ChangeQty product str -> 
      case toInt str of 
        Ok qty -> 
          case changeQty model.cart product qty of 
            Ok cart -> 
              { model | cart = cart, error = Nothing } 
 
            Err msg -> 
              { model | error = Just msg } 
 
        Err msg -> 
          { model | error = Just msg }

We use type Maybe String for the error attribute in our model. Maybe is another type which can either hold Nothing or a value of a specific type.

After updating the view function as follows:

view model = 
  section [style [("margin", "10px")]] 
    [ stockView model.stock 
    , cartView model.cart 
    , errorView model.error 
    ] 
errorView : Maybe String -> Html Msg 
 
errorView error = 
  case error of 
    Just msg -> 
      p [style [("color", "red")]] [ text msg ] 
 
    Nothing -> 
      p [] [] 

You should see this:

Attempting to enter a non-numeric value (e.g. “1a”) would result in an error message as shown in the screenshot above.

World of Packages

Elm has its own repository of open source packages. With the package manager for Elm, it becomes a breeze to leverage this pool of packages. Although the size of the repository is not comparable to some other mature programming languages like Python or PHP, the Elm community is working hard to implement more packages every day.

Notice how the decimal places in prices rendered in our view are inconsistent?

Let’s replace our naive use of toString with something better from the repository: numeral-elm.

cartProductView item = 
  tr [] 
    [ td [] [ text item.product.name ] 
    , td [ align "right" ] [ text (formatPrice item.product.price) ] 
    , td [ align "center" ] 
      [ input 
        [ value (toString item.qty) 
        , onInput (ChangeQty item.product) 
        , size 3 
        --, type' "number" 
        ] [] 
      ] 
    , td [ align "right" ] [ text (formatPrice (itemSubtotal item)) ] 
    ] 
 
formatPrice : Float -> String 
 
formatPrice price = 
  format "$0,0.00" price 

We are using the format function from the Numeral package here. This would format the numbers in a way we typically format currencies:

100.5 -> $100.50
15.36 -> $15.36
21.15 -> $21.15

Automatic Documentation Generation

When publishing a package to the Elm repository, documentation is generated automatically based on the comments in the code. You can see it in action by checking out the documentation for our Cart module here. All of these were generated from the comments seen in this file: Cart.elm.

A True Debugger for Front-end

Most obvious issues are detected and reported by the compiler itself. However, no application is safe from logical errors.

Since all data in Elm is immutable and everything happens through messages passed to the update function, the entire flow of an Elm program can be represented as a series of model changes. To the debugger, Elm is just like a turn-based strategy game. This allows the debugger to perform some really amazing feats, such as traveling through time. It can move back and forth through the flow of a program by jumping between various model changes that happened during the lifetime of a program.

You can learn more about the debugger here.

Interacting with a Back-end

So, you say, we have built a nice toy, but can Elm be used for something serious? Absolutely.

Let’s connect our cart front-end with some asynchronous back-end. To make it interesting, we will implement something special. Let’s say we want to inspect all carts and their contents in real-time. In real life, we could use this approach to bring some extra marketing/sales capabilities to our online shop or marketplace, or make some suggestions to the user, or estimate required stock resources, and much more.

So, we store the cart on the client side and also let the server know about each cart in real time.

To keep things simple, we will implement our back-end using Python. You can find the full code for the back-end here.

It’s a simple web server that uses a WebSocket and keeps track of the contents of the cart in-memory. To keep things simple, we will render everyone else’s cart on the same page. This can easily be implemented in a separate page or even as a separate Elm program. For now, every user will be able to see the summary of other users’ carts.

With the back-end in place, we will now need to update our Elm app to send and receive cart updates to the server. We will use JSON to encode our payloads, for which Elm has excellent support.

CartEncoder.elm

We will implement an encoder to convert our Elm data model into a JSON string representation. For that, we need to use the Json.Encode library.

module CartEncoder exposing (cart) 
 
import Cart2 exposing (Cart, Item, Product) 
import List exposing (map) 
import Json.Encode exposing (..) 
 
product : Product -> Value 
product product = 
  object 
    [ ("name", string product.name) 
    , ("price", float product.price) 
    ] 
 
item : Item -> Value 
item item = 
  object 
    [ ("product", product item.product) 
    , ("qty", int item.qty) 
    ] 
 
cart : Cart -> Value 
cart cart = 
  list (map item cart) 

The library provides some functions (such as string, int, float, object, etc.) that take Elm objects and turn them into JSON encoded strings.

CartDecoder.elm

Implementing the decoder is a bit trickier as all Elm data has types and we need to define what JSON value needs to be converted to which type:

module CartDecoder exposing (cart) 
 
import Cart2 exposing (Cart, Item, Product) -- decoding for Cart 
import Json.Decode exposing (..) -- will decode cart from string 
 
cart : Decoder (Cart) 
cart = 
  list item -- decoder for cart is a list of items 
 
item : Decoder (Item) 
item = 
  object2 Item -- decoder for item is an object with two properties: 
    ("product" := product) -- 1) "product" of product 
    ("qty" := int) -- 2) "qty" of int 
 
product : Decoder (Product) 
product = 
  object2 Product -- decoder for product also an object with two properties: 
    ("name" := string) -- 1) "name" 
    ("price" := float) -- 2) "price" 

Updated Elm Application

As the final Elm code is a bit longer, you can find it here. Here is a summary of changes that have been made to the front-end application:

We have wrapped our original update function with a function that sends changes to contents of the cart to the back-end every time the cart is updated:

updateOnServer msg model = 
  let 
    (newModel, have_to_send) = 
      update msg model 
  in 
    case have_to_send of 
      True -> -- send updated cart to server 
        (!) newModel [ WebSocket.send server (encode 0 (CartEncoder.cart newModel.cart)) ] 
 
      False -> -- do nothing 
        (newModel, Cmd.none) 

We also have added an additional message type of ConsumerCarts String to receive updates from the server and update the local model accordingly.

The view has been updated to render the summary of others’ carts using the consumersCartsView function.

A WebSocket connection has been established to subscribe to the back-end to listen for changes to others’ carts.

subscriptions : Model -> Sub Msg 
 
subscriptions model = 
  WebSocket.listen server ConsumerCarts 
server = 
  "ws://127.0.0.1:8765" 

We have also updated our main function. We now use Html.program with additional init and subscriptions parameters. init specifies the initial model of the program and subscription specifies a list of subscriptions.

A subscription is a way for us to tell Elm to listen for changes on specific channels and forward those messages to the update function.

main = 
  Html.program 
    { init = init 
    , view = view 
    , update = updateOnServer 
    , subscriptions = subscriptions 
    } 
 
init = 
  ( Model [] -- empty cart 
    [ Product "Bicycle" 100.50 -- stock 
    , Product "Rocket" 15.36 
    , Product "Bisquit" 21.15 
    ] 
    Nothing -- error (no error at beginning) 
    [] -- consumer carts list is empty 
  , Cmd.none) 

Finally we have handled the way we decode ConsumerCarts message that we receive from the server. This ensures that data we receive from external source will not break the application.

ConsumerCarts message -> 
  case decodeString (Json.Decode.list CartDecoder.cart) message of 
    Ok carts -> 
      ({ model | consumer_carts = carts }, False) 
 
    Err msg -> 
      ({ model | error = Just msg, consumer_carts = [] }, False) 

Keep Your Front-ends Sane

Elm is different. It requires the developer to think differently.

Anyone coming from the realm of JavaScript and similar languages will find themselves trying to learn Elm’s way of doing things.

Ultimately though, Elm offers something that other frameworks - even the most popular ones - often struggle to do. Namely, it provides a means to build robust front-end applications without getting tangled in huge verbose code.

Elm also abstracts away many of the difficulties that JavaScript poses by combining a smart compiler with a powerful debugger.

Elm is what front-end developers have been yearning for, for so long. Now that you have seen it in action, take it for a spin yourself, and reap the benefits by building your next web project in Elm.

Hire a Toptal expert on this topic.
Hire Now
Stas Davydov

Stas Davydov

Verified Expert in Engineering

Surat Thani, Thailand

Member since April 21, 2015

About the author

Stanislav’s 18+ years in organizations of all sizes have focused on eCommerce, complex web services, friendly UI, and data mining/scraping.

authors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

PREVIOUSLY AT

Emergn

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

Join the Toptal® community.