Ensure the reliability of your web front-end with Elm

Is debugging your web front-end a constant struggle with intricate event chains? Have you ever attempted to refactor a UI built on jQuery, Backbone.js, or other popular JavaScript frameworks?

The most frustrating part is often tracing these unpredictable event sequences and preemptively addressing all potential issues. A true nightmare!

I’ve always sought ways to escape this dreaded aspect of front-end development. While Backbone.js provided much-needed structure, its verbosity for simple tasks wasn’t ideal.

Make Your Web Front-end Reliable with Elm

Then I discovered Elm.

Based on Haskell, Elm is a statically typed functional language with a simpler specification. Its Haskell-built compiler translates Elm code into JavaScript.

Initially designed for front-end development, Elm has found its way into server-side programming as well.

This article explores how Elm revolutionizes front-end development and introduces its functional programming basics. We’ll build a simple shopping cart application to illustrate.

Why Elm?

Elm boasts numerous advantages for clean front-end architecture, offering better HTML rendering performance advantages compared to popular frameworks, even React.js. It empowers developers to write code that practically eliminates common runtime exceptions plaguing dynamically typed languages like JavaScript.

The compiler automatically infers types and provides user-friendly error messages, highlighting any potential issue during development.

With 36,000 lines of Elm in production for over a year, NoRedInk hasn’t encountered a single runtime exception. [Source]

You don’t need to convert your entire JavaScript application to experiment with Elm. Its excellent JavaScript interoperability allows you to rewrite even small sections in Elm.

Elm also has excellent documentation that not only explains its features but also guides you in building front-ends using The Elm Architecture, promoting modularity, code reuse, and testing.

Building a Simple Cart

Let’s start with a concise Elm code snippet:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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)

Text preceded by -- denotes a comment in Elm.

We define a cart as an item list, each a record with two fields: product and quantity. Products are records with name and price.

Adding a product involves checking for its existence in the cart. If present, we do nothing; otherwise, we add it as a new item. This check involves filtering the list, matching items with the product, and checking if the resulting list is empty.

The subtotal calculation iterates over cart items, multiplying quantities by prices and summing the results.

This represents a minimalistic cart and its functions. We’ll gradually enhance it into a complete web component, or “program” in Elm terms.

Let’s begin by adding types to our identifiers. While Elm infers types, explicit annotations maximize the compiler’s capabilities.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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 

Type annotations enable the compiler to catch potential runtime exceptions early.

However, Elm’s benefits extend further. The Elm Architecture provides a simple yet familiar pattern for structuring front-ends:

  • Model: Represents the application’s state.
  • View: Provides a visual representation of the state.
  • Update: Offers a way to modify the state.

If you consider the update component as the controller, it resembles the Model-View-Controller (MVC) paradigm.

As a pure functional language, Elm enforces data immutability, preventing model modification. Instead, we create new models based on previous ones using update functions.

This immutability eliminates function side effects, unlocking possibilities like Elm’s time-traveling debugger, discussed later.

Views re-render whenever model changes necessitate UI updates, ensuring consistent results for the same model data, much like pure functions returning consistent outputs for identical inputs.

Starting with the Main Function

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

Those familiar with React will appreciate Elm’s package for defining HTML elements. This eliminates reliance on external templating languages.

The Html package provides wrappers for HTML elements:

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

All Elm programs begin execution with the main function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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, main initializes the program with models, a view, and an update function. We define products and their prices, assuming an unlimited supply for simplicity.

A Simple Update Function

The update function brings our application to life.

It receives a message and modifies the state accordingly. We define it as a function taking two parameters (message and current model) and returning a new model:

1
2
3
4
5
6
7
8
type Msg = Add Product 

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

Currently, we handle a single message, Add product, calling the add method on cart with the product.

The update function’s complexity will grow with the application.

Implementing the View Function

Next, we define the cart’s view.

This function translates the model into HTML. However, it’s not merely static; it emits messages back to the application based on user interactions and events.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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 familiarly named functions for common elements (e.g., section generates a <section> element).

The style function (from Html.Attributes) generates an object applicable to the section function for setting the element’s style attribute.

For reusability, it’s advisable to split the view into separate functions.

We embed CSS and layout attributes directly into the view code for simplicity. However, libraries exist to streamline process of styling from Elm code.

Note the button element and its associated Add product message for the click event.

Elm handles callback function binding, event generation, and update function invocation with relevant parameters.

Finally, let’s implement the remaining view component:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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)) ] 
    ] 

This renders the cart’s contents. While it doesn’t emit messages, it must return Html Msg to qualify as a view.

The view not only lists items but also calculates and displays the subtotal based on the cart’s contents.

The complete Elm code is available here.

run the Elm program now would yield something like this:

How does it work?

The main function initializes the program with an empty cart and hardcoded products.

Each “Add to Cart” button click sends a message to the update function, which modifies the cart and creates a new model. Model updates trigger view function invocation to regenerate the HTML tree.

Elm’s Virtual DOM approach (similar to React) ensures only necessary UI changes are applied for optimal performance.

Beyond Type Checking

Elm’s static typing enables the compiler to verify much more than just types.

Let’s modify our Msg type and observe the compiler’s response:

1
type Msg = Add Product | ChangeQty Product String 

We’ve added a new message type for changing product quantity. However, running the program without handling this message in the update function results in the following error:

Towards a More Functional Cart

We used a string for the quantity value previously, as it originates from an <input> element.

Let’s add a changeQty function to the Cart module, encapsulating implementation for potential future modifications without impacting the module’s API.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
{-| 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 avoid assumptions about function usage. While qty is guaranteed to be an Int, its value is unknown. We validate the value and report an error if invalid.

We update the update function accordingly:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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 

The string quantity parameter from the message is converted to a number before use. Invalid number strings are reported as errors.

Here, we maintain the model upon encountering an error. Alternatively, we could update it to display the error as a message in the view:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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 the Maybe String type for the error attribute in our model. Maybe either holds Nothing or a specific type’s value.

After updating the view function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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:

Entering a non-numeric value (e.g., “1a”) now triggers an error message, as shown in the screenshot.

Leveraging Packages

Elm has its own repository of open-source packages. The Elm package manager simplifies leveraging this resource. While smaller than repositories for languages like Python or PHP, the Elm community actively contributes new packages.

Notice the inconsistent decimal places in our view’s rendered prices?

Let’s replace our rudimentary toString usage with the superior numeral-elm package.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
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 utilize the format function from the Numeral package. This formats numbers in a typical currency format:

1
2
3
100.5 -> $100.50
15.36 -> $15.36
21.15 -> $21.15

Automatic Documentation

Package publication to the Elm repository triggers automatic documentation generation based on code comments. See this in action with our Cart module documentation here. These were generated from comments within this file: Cart.elm.

A True Front-end Debugger

The compiler detects and reports most obvious issues. However, logical errors can still occur.

Elm’s immutable data and message-driven update function allow representing the entire program flow as a series of model changes. The debugger treats Elm like a turn-based strategy game, enabling features like time travel. It traverses the program flow by jumping between model changes throughout the program’s lifecycle.

Learn more about the debugger here.

Back-end Integration

You might wonder about Elm’s practicality beyond toy examples. Rest assured, it’s fully capable.

Let’s connect our cart front-end to an asynchronous back-end. For a unique twist, we’ll enable real-time inspection of all carts and their contents. In a real-world scenario, this could power marketing/sales features, provide user suggestions, estimate stock requirements, and more.

We’ll store the cart client-side and update the server in real-time.

We’ll use Python for our back-end for simplicity. The complete back-end code is available here.

It’s a basic web server using a WebSocket to track cart contents in memory. For simplicity, we’ll render everyone’s cart on the same page. This could easily be separated into different pages or Elm programs. Currently, users can view a summary of other users’ carts.

With the back-end established, let’s update our Elm app to exchange cart updates with the server. We’ll utilize JSON for payload encoding, which Elm handles seamlessly.

CartEncoder.elm

We’ll implement an encoder to convert our Elm data model into a JSON string. This requires the Json.Encode library.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 functions like string, int, float, and object to convert Elm objects into JSON encoded strings.

CartDecoder.elm

Implementing the decoder is more intricate due to Elm’s typed data. We need to map JSON values to their corresponding Elm types:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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

The final Elm code is more extensive and can be found here. Here’s a summary of the front-end application’s modifications:

We’ve wrapped the original update function to send cart content changes to the back-end upon each update:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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’ve added a new message type, ConsumerCarts String, to receive server updates and update the local model accordingly.

The view now renders other users’ cart summaries using the consumersCartsView function.

A WebSocket connection subscribes to the back-end for updates to other users’ carts.

1
2
3
4
5
6
subscriptions : Model -> Sub Msg 
 
subscriptions model = 
  WebSocket.listen server ConsumerCarts 
server = 
  "ws://127.0.0.1:8765" 

The main function has also been updated. We now use Html.program with additional init and subscriptions parameters. init sets the initial model, while subscription specifies a list of subscriptions.

Subscriptions instruct Elm to listen for changes on specific channels and forward messages to the update function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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 handle decoding the ConsumerCarts message received from the server, ensuring data integrity.

1
2
3
4
5
6
7
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) 

Maintaining Front-end Sanity

Elm is different, requiring a shift in developer mindset.

Those accustomed to JavaScript might find themselves adapting to Elm’s approach.

However, Elm excels where other frameworks, even popular ones, often falter. It provides a way to build robust front-end applications without succumbing to verbose code.

Elm’s smart compiler and powerful debugger abstract away many JavaScript-related challenges.

It’s the answer to front-end developers’ long-standing desires. Now that you’ve seen it in action, give it a try and experience the benefits of building your next web project with Elm.

Licensed under CC BY-NC-SA 4.0