Front-End Software

Shifting from OOP Web Components to FP: A brief look at Elm

Hello, Functional Programming; Goodbye Local State

The fundamental constraint in functional programming is that you cannot re-assign variables. Local state is only possible if a component can internally re-assign variables, so…no local state!

This means that we actually can’t meaningfully build up components in the OO sense any more. We don’t lose the ability to modularise our code however — more on that later — we instead are forced to rethink how we divvy up state.

Think of the entire application state as existing in a JavaScript object. Let’s call it model, with a type Model. Suppose that the HTML output of our application is a pure function, which takes model as an input.

function view(model: Model) : HTML

So if I have models m1 and m2, and these two models have the same values (i.e. m1 == m2), then they will output the exact same HTML (i.e. view(m1) == view(m2)).

How do we have state changes then?

Well, suppose that in our rendered HTML, we have event handlers which map events into messages that tell us how the state should change. So there is a type Msg, which tells me all the different ways that the state can change, and a function update, which takes a Msg and a Model and gives me a new model (again, a pure function). So:

function update(msg: Msg, model: Model): Model

When my HTML nodes dispatch their events, they map to a msg; that msg and current model are given to update(), and I get my next piece of state.

Then to bootstrap the application, I just need to have an initial model to kickstart the process.

This is what Elm does for you! You define:

  • Model for your valid states
  • Msg for your state-deltas
  • update to explicitly define how to transition from one model to another
  • view for how the output should look, and to map DOM events to Msgs
  • init to define an initial state

then hand them off to Elm’s compiler, and the outputted JavaScript will handle the DOM updates.

Back to our todo example, we can give definitions like the following:

-- Main.elm
module Main exposing (main)

import Html -- used for rendering HTML nodes
import Html.Attributes -- used for providing HTML attributes
import Html.Events -- used for binding to DOM events

main = Browser.sandbox
    { init = 
        { inputText = "Type your todo here..."
        , todos = []
    , update = update
    , view = view

type alias Model = 
    { inputText : Description
    , todos : List Todo

-- Some aliases to improve the readability of the Msg below...
type alias Todo =
    { id : TodoId
    , description : Description
    , status : TodoStatus
    , mode : ViewMode

type alias TodoId = String
type alias Description = String
type alias TodoStatus = Bool

type ViewMode
    = View
    | Edit Description

-- MSG
type Msg
    = ChangeInputText Description
    | AddNewTodo Description
    | ChangeTodoStatus TodoId TodoStatus
    | EnableEditMode TodoId
    | ChangeEditModeText Description
    | ChangeTodoDescription Description
    | DeleteTodo TodoId

update : Msg -> Model -> Model
update msg model =
    case msg of
        ChangeInputText desc ->
        DeleteTodo id ->

view : Model -> Html.Html Msg
view model =
    Html.div []
        [ Html.h1 [] [ Html.text "Todo List" ]
        , Html.form [ Html.Events.onSubmit (AddNewTodo model.inputText) ]
            [ Html.input [ Html.Events.onInput ChangeInputText, Html.Attributes.value model.inputText ] [] ]
        , viewTodos model.todos

viewTodos : TodoList -> Html.Html Msg
viewTodos todos = ...

The massive benefit of this is that we have a deterministic, fully predicable state-space. We can effectively draw a directed graph of all the state classes that can be entered, just based on the Msg, Model and update. Was it obvious from the component-based version of the app that these were all of the available state transitions? It would have been easy to end up in a situation where we had invalid state, simply because we couldn’t reliably tell what the state-space was.

We still want to break code down into smaller pieces though, since cognitively it is quite a lot to have in one head. I’ve also deliberately left out pieces of the implementation because I wanted to leave that for the next section.