Elm Language

Lou Mauget JavaScript, Programming, Single-Page Application, Technology Snapshot Leave a Comment

This blog is about my dalliance with Elm; a purely functional, statically-typed language that has type inference. It compiles to JavaScript. Functional programming is compelling, but heretofore, I’d only woven cherry-picked techniques into large object-oriented projects. In Functional Programming parlance, I’m partially applied! The times, they are a-changin’.

In this article, I’ll:

  • touch on the reasoning for giving a nod to functional languages and data immutability;
  • move on to Elm; a blazing-fast, statically typed, purely functional browser-side language that compiles to JavaScript and follows the principles of functional reactive programming;
  • survey background items and the Elm environment;
  • show a simple type-and-click application, followed by a more realistic To-do application;
  • end with my impressions from functional-programming semi-outsider point-of-view.

Why

Nowadays, Moore’s Law manifests by increasing CPU cores, not by doubling core speed. That compels us to exploit increased hardware potential via an altered coding approach. Immutable data combined with pure first-class functions help us to manage fragile state across multiple CPU cores.

Elm

Enter Elm, a browser client language. It resembles a subset of Haskell, a pure functional language: really, really pure. Haskell compiles to a native executable. Elm compiles to JavaScript that targets a browser. Previous Haskell play helped me learn Elm, while Elm, in turn, increased my Haskell knowledge.

Elm’s application space is a single-page application. This is the realm of React, Vue, or Angular. Elm can call or be called by JavaScript as well. We could restrict the use of Elm to a part of a page, a project, or a migration. Like Haskell, Elm code, when compiled error-free, cannot cause runtime exceptions. Sure, that does not guarantee that program logic is correct, but sane typing mitigates issues in seeking correct logic. To aid in diagnosing compilation issues, Elm produces the most-helpful error messages that I’ve seen in any language.

DOM manipulation is a cycle-eating hotspot in SPA code. Elm minimizes DOM manipulation as does React, and modern Vue or Angular. These ignore unnecessary DOM changes by means of shadow DOM diffing. Surprisingly, Elm renders significantly faster than that competition. See https://elm-lang.org/blog/blazing-fast-html-round-two.

Older SPAs often maintained state in the DOM: “disable that input field if this radio button is set.” That approach is bad due to the DOM performance issue, and worse, because it mixes concerns of rendering with state.

Instead, Elm reacts to model changes, rendering DOM elements accordingly.

An Elm application’s state resides in an immutable model record. When view code emits an event (e.g. user clicks a button), the Elm runtime fields the event, passing it to a stipulated update function. The update function may construct a new model from the current model using information from the event. The new model is the return value to the runtime.

The runtime internally carries out any model replacement by limiting replacement to isolated structural parts of the input model. This structural mode of replacement is the usual approach in immutability implementations.

At this point, the runtime calls a configured view function. The view renders a new DOM based upon model state, by using the DOM delta approach mentioned earlier. Elm does not use a template language for rendering but instead uses a composition of pure functions to create HTML elements, attributes, text, and contained elements.

After rendering, the runtime sleeps, awaiting another external event. At each event, a new generation of the application model circulates at each interaction between view code and update code, as shown in this figure below:

That is the Elm run cycle.

Development Environment

You can develop for Elm on Linux, Mac OS, or Windows – anywhere you can edit a text file and execute the development commands:

  • elm repl – interact with Elm expressions on a command line
  • elm make – most-general way to compile an Elm project to either JavaScript or to HTML
  • elm reactor – browser-based development environment, doc, and test server
  • elm install – adds https://package.elm-lang.org/ dependencies to elm.json (think: npm and package.json)

Run elm help to receive help that is amazingly helpful.

The elm.json project file declares the location of test and application source files, the version of the Elm environment, and any included library dependencies.

{
    "type": "application",
    "source-directories": [
        "src"
    ],
    "elm-version": "0.19.0",
    "dependencies": {
        "direct": {
            "elm/browser": "1.0.1",
            "elm/core": "1.0.2",
            "elm/html": "1.0.0"
        },
        "indirect": {
            "elm/json": "1.1.3",
            "elm/time": "1.0.0",
            "elm/url": "1.0.0",
            "elm/virtual-dom": "1.0.2"
        }
    },
    "test-dependencies": {
        "direct": {},
        "indirect": {}
    }
}

I used WebStorm IDE with an Elm plugin because I already had WebStorm. Any type violations received red underlines, and when I repaired a type flow, the red lines disappeared. Successfully compiled code will never throw an exception; code having type issues refuses to execute.

A good text editor, such as Sublime 3 or Atom, along with an Elm plugin, is an alternative to an IDE. Issue Elm commands to carry out compilation instantly.

See Also:  React Native With Expo

Modules

Elm is a functional language having pure first-class functions. A first-class function can take a function as input data or return a function in result data.

An object can hold state, but a pure function cannot. Elm has no objects. An Elm module’s state resides in that immutable model we showed earlier, never in any function. At each external event, the designated Elm update function creates a new model from the old model and message data. If we wanted to make a component-like entity per page in Elm, we could use an Elm module per page.

Elm application developers use such modules to help grow a program in a sane manner. An idiomatic page module’s non-library code resides in a single text file. We segregate consecutive init, update, and view sections under header comments in the file. For an accepted source layout, see https://guide.elm-lang.org/webapps/structure.html.

Type Information

Refer to https://guide.elm-lang.org/core_language.html to learn about basic Elm syntax. Notice that type information appears last in a definition, not first, contrary to Java, JavaScript, or C. This allows trailing explicit type information to possibly be omitted. Elm is strongly typed, but Elm’s type analysis can often infer the type from flow analysis. I’ve found that designing code for Elm is first about getting a type flow arranged.

Function Signature

A function declaration consists of a function name (think verb) followed by zero or more arguments (think nouns). The arguments’ separator is ->. The final item is the required return type.

Elm functions actually take only one argument internally! Elm internally composes a source function by applying arguments one-by-one, returning a new function at each application except the final step. This single-argument partial application is called currying. A call to a multi-argument Elm function compiles to a composition of curried functions. Reference: https://en.wikipedia.org/wiki/Currying.

Here’s a contrived double-argument Elm function declaration:

concatenate: String -> String -> String

Its corresponding function definition has an = that delimits the body that creates the functional result. An Elm function’s result is the value of its final expression.

concatenate stringA stringB = stringA ++ stringB

The optional type declaration normally precedes the declaration on a line above the declaration. Here’s an operational function signature and definition from my TODO application:

generateKey : Seq -> TodoKey
generateKey seq =
    "td" ++ String.fromInt seq

The function returns a string that is a function of an input sequence integer.

HTML View

Elm carries out View rendering through an HTML library that creates each desired element from a function taking parameters for attributes, events, and content. A render function for an HTML element expects an attribute array parameter and a content array parameter. For example, the following represents an HTML div that has no attributes, signified by the empty array first argument.

div [] [ text (renderText model) ]

It has a content array second argument consisting of a text function that takes a renderText function argument that takes a model argument. This shows first-class functions in practice. Note that content could have been another render element function instead.

The following is a button having an onClick attribute that sends a reset message:

button [ onClick Reset ] [ text "Reset" ]

See the simple example source in the next section, for a small complete view rendering example.

Reduced Example

This typical Elm Hello World echoes a field input to text and can reset the input via a button click. This is a screenshot:

Refer to the source below. The main is the entry function that declares defines the names of the init, update, and view functions that handle the model in the runtime cycle. I used an Elm custom type to define a Model custom type. It is simply a String in this case. A Msg type defines the type of message used to communicate model change requests.

Simple Example Source

To run it:

  1. Ensure that Elm infrastructure is installed: https://guide.elm-lang.org/install.html
  2. clone the project repo https://guide.elm-lang.org/install.html
  3. build ./index.html using the elm command: elm make src/Main.elm
  4. open ./index.html in a browser.
module Main exposing (..)

import Browser
import Html exposing (Html, Attribute, div, input, text, button, br)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput, onClick)

-- MAIN


main =
  Browser.sandbox { init = init, update = update, view = view }


-- MODEL


type alias Model =
  { content : String
  }


init : Model
init =
  { content = "" }


-- UPDATE


type Msg
  = Change String
  | Reset


update : Msg -> Model -> Model
update msg model =
  case msg of

    Change newContent ->
        { model | content = newContent }

    Reset  ->
        { model | content = "" }



-- VIEW

renderText : Model -> String
renderText model =
    if String.isEmpty model.content
    then ""
    else "Hello, " ++ model.content ++ "!"


rootStyle : Attribute msg
rootStyle =
    attribute
        "style"
        "background-color: #eeeeee; width: 10rem; padding: 2rem; margin: 4rem; border: solid 1px black"


view : Model -> Html Msg
view model =
  div [ rootStyle ]
    [ div [] [ text (renderText model) ]
    , br [] []
    , input [ placeholder "Enter text", value model.content, onInput Change, size 13, maxlength 12, autofocus True] []
    , br [] []
    , br [] []
    , button [ onClick Reset ] [ text "Reset" ]
    ]

Interoperating with JavaScript

We invoke compiled Elm via a native JavaScript calling routine. For example, the < script > block below calls Elm-compiled JavaScript that was loaded in a previous < script > block. The example entry function is Elm.Main.init because I named the source Elm module Main and I named its entry function init. Look at the init function’s object parameter. Its node key names a DOM element that receives the Elm view rendered DOM subtree. But what is that flags key?

See Also:  Design Pattern: Microservice Authentication + Authorization
JavaScript Elm invoker
< script >
    const storeName = 'cache';
    const cache = JSON.parse(localStorage.getItem(storeName));

    const app = Elm.Main.init({
        node: document.getElementById('app'),
        flags: cache
    });

    app.ports.cache.subscribe(function(data) {
        localStorage.setItem(storeName, JSON.stringify(data));
    });

< / script >

Elm has two optional points of integration with its invoking JavaScript: flags and ports. The flags are a means to pass a value to the Elm program during its initialization. The ports are a single-direction asynchronous message-passing mechanism between JavaScript and Elm. To pass messages in both directions, use two ports. Refer to https://guide.elm-lang.org/interop/ports.html for flags and ports. The asynchronous message-passing nature of ports provides a barrier to external JavaScript code causing a runtime exception in Elm code.

Realistic Example

If you expected an example TODO application as a realistic CRUD example, then here it is: https://github.com/mauget/elm-todo.git.

It uses browser local storage to cache the model. I used flags and an inbound port to cache the model under the direction of my Elm code. The signature of the Elm side of the port looks like this:

	port cache : Maybe Model -> Cmd msg

Recall the JavaScript Elm init call shown previously? When Elm code invokes that cache function, the result is a Cmd message that causes a call to that JavaScript subscribe listener repeated here:

app.ports.cache.subscribe(function(data) {
    localStorage.setItem(storeName, JSON.stringify(data));
});

The remainder of the TODO program is an idiomatic Elm module that runs the Elm update/view loop we discussed. A bit of page-loaded CSS provides layout and gingerbread. Refer to the source on GitHub, along with the Elm official guide https://guide.elm-lang.org/.

To run:

  1. Ensure that Elm infrastructure is installed: https://guide.elm-lang.org/install.html
  2. Clone the repo from https://github.com/mauget/elm-todo.git
  3. Compile Main.elm to elm.js using a command from the project root:elm make src/Main.elm --output=elm.js
  4. Open ./index.html in a browser; no server needed for this application

Impressions

The following are my takeaways from playing with Elm. Notice that some cons and pros are at odds, like opposing sides of that old double-sided sword…

Cons

  • Strong typing feels like interference at times
  • Elm community and ecosystem is thin
  • Talent hiring pool seems small
  • Coding HTML from functions feels like coding HTML from servlets
  • New features seem slow to emerge due to the slow pace of releases
  • Elm is a SPA language only

Pros

  • Strong typing reduces unit test numbers
  • Immutability + Strong typing + pure first-class functions == crash-proof
  • Pure functions are easy to confidently test
  • Newbie-friendly
  • Fast compiler
  • The most-helpful compiler messages ever
  • Workable for a small part of an application or page
  • Asynchronous JavaScript ports are a crash barrier
  • Slow thoughtful pace of releases with few breaking changes (compare Node.js)
  • Blazing fast DOM updates
  • Elm community is warm and welcoming
  • Immutable collection contents are also immutable (compare Immutable.js)
  • Elm is a SPA language only

Would I pick Elm if I were an architect who had to choose a language or framework for a SPA application? I would, provided that I could find Elm talent for the project. I’d have to consider both development and future maintenance. I reason that maintenance would consist more of features and less of defects because the compiler and ease of unit testing could short-circuit many future bugs.

Wrap

We…

  • looked at Elm, a fast, statically typed, purely functional browser-side language that compiles to JavaScript;
  • reasoned about the need for a language having characteristics of immutability, strong typing, and first-class pure functions;
  • carried out a coarse overview of the characteristics of Elm, providing references for a deeper dive;
  • showed a minimal interactive application that exercises the Elm model flow, followed by a more realistic TODO application;
  • presented subjective impressions of Elm.

References

Elm Official Guide: https://guide.elm-lang.org/
Elm Packages: https://package.elm-lang.org
Public Domain Logo: https://commons.wikimedia.org/w/index.php?curid=51314022

What Do You Think?