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

Published

A couple of months ago, someone asked on the JavaScript subreddit how functional programmers handle dealing with unavoidably immutable actions, like DOM manipulation. I gave my $0.02, saying that imposing functional programming constraints on your DOM-specific code is more trouble than it’s worth, and to leverage FP on the boundaries of your code that don’t interact with it. I ended with a recommendation for Elm, which is a neat little functional language that compiles to JavaScript, if someone is fully intent on writing more of their application in a functional style.

Recently I’ve spent some time playing more with Elm, and I’ve actually been enjoying it quite a lot! The documentation has improved immensely over the last few years and it is a lot more friendly getting on-boarded. I learned functional programming about 6 years ago at university – learning Haskell for the FP course and Principles of Programming Languages course) – but never spent time applying it in a UI context, and still found the documentation/supplementary material on Elm a bit lacking (until recently). So today I thought I’d outline some of the principle differences in building UIs between the component-based model (which we see in the likes of React, Vue.js, etc…) and Elm.

*Note: this article will assume knowledge of JSX for the component examples, as well as and basic knowledge of Elm’s syntax for the Elm code samples.*

The Component Model and Object Orientation

When building modern web components, we use a distinctly Object Oriented model. We decompose the application into a collection of components, which represent a fragment of the overall user interface and handle some part of the application’s state. We wire these components together by structuring them in a tree – as we do with HTML – and then allow the components to communicate with each other via a defined interface. In OOP this interface is the publicly exposed object methods, and with web components this is the publicly subscribable component events. This can be accessed programatically (e.g. elem.addEventListener() or, more commonly via passing callback functions as attributes to the respective elements (e.g. <elem onSomeEvent={myCallback}/>)). We pass data from parents to children via attributes (also called props in some frameworks), and from children to parents via callbacks; updating local state where necessary, and re-rendering the subtree where necessary.

In the case of a basic todo-list application, you’d probably have a <Todo> component which represents an individual item which you can “tick” off as done, change the description for, or delete. You’d also have a <TodoList> component which renders a list of Todo components based on its internal list of todo objects. Then in your top level <App>, you might have a <form> that users can type text into, and when they submit the form it adds the typed text as a new todo.

class App extends React.Component {
    // omitted other methods for brevity...
    render() {
        return <div>
            <form onSubmit={this.addTodo}>
                <input value={this.inputText} onChange={this.handleInputText}/>
            </form>
            <TodoList todos={this.state.todos}/>
        </div>;
    }
}


class TodoList extends React.Component {
    // ...
    render() {
        return <ul>
            {this.state.todos.map(todo => (<li>
                <Todo
                    id={todo.id}
                    description={todo.description}
                    done={todo.done}
                    onChange={this.handleChange}
                    onDelete={this.handleDelete}
                />
            <li>))}
        </ul>;
    }
}


class Todo extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            mode: "view",
            description: props.description,
        }

        // ...
    }

    // ...

    render() {
        switch (this.state.mode) {
            case "edit":
                return <div class="edit-mode">
                    <input value={this.state.description} onChange={this.handleChange}/>
                </div>;
            default:
                return <div>
                    <input type="checkbox" checked={this.props.done} onChange={this.props.onChange}/>
                    <span>{this.props.description}</span>
                    <a href="#" onClick={this.enableEditMode}>Edit</a>
                    <a href="#" onClick={this.props.onDelete}>Delete</a>
                </div>;
        }
    }
}

This model is great for reducing the complexity of a massive user interface because it allows us to think of it in terms of smaller – but still useful – “blocks”, which slot together to form the overall application. You don’t need to have the whole application “in your head” in order to build it - you can build it out in isolated pieces, then wire together the desired pieces. You can (read: should!) write tests on this “wiring” to ensure that the components behave together as expected, but this isn’t a massive trade-off to make when it can hugely increase the cognitive ease of understanding the application.

That trade-off however comes with an acknowledgement that understanding the full state-space in a predictable, reliable fashion, is almost impossible beyond very simple applications. In some cases, this can be quite undesirable. For instance, if there is an entire class of bugs that can arise from my application entering particular states, how can I safely prevent the user from ever being able to enter these states, when it requires synchronisation across so many component boundaries?

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

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

-- MODEL
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
update : Msg -> Model -> Model
update msg model =
    case msg of
        ChangeInputText desc ->
            ...
        ...
        DeleteTodo id ->
            ...

-- VIEW
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.

Modularisation around Data Structures

Moving away from a component/class based model, we instead create modules based around pure data structures, and ways of manipulating said data.

So if we were to break up the todo list, we wouldn’t break it up into the sorts of files you’d associate with components, such as Components/Todo.elm, Components/TodoList.elm, where each has the view/render logic as well as local state (since again, no local state). Instead, we think of just the data that we’re trying to manipulate, and provide an API for doing so.

Imagine that there was a Todo module, which handled the different data transitions for a todo-list. How might it be integrated into the previous code?

-- Main.elm
module Main exposing (main)

import Todo -- hypoethetical module
import Html
import Html.Attributes
import Html.Events

-- BOOTSTRAP IS THE SAME
main = ...

-- MODEL
type alias Model = 
    { inputText : Todo.Description
    , todos : Todo.List
    }

-- MSG
type Msg
    = ChangeInputText Todo.Description
    | AddNewTodo Todo.Description
    | ChangeTodoStatus Todo.Id Todo.Status
    | EnableEditMode Todo.Id
    | ChangeEditModeText Todo.Description
    | ChangeTodoDescription Todo.Description
    | DeleteTodo Todo.Id

This module will include methods for manipulating lists to todos. So update will use them for creating new models:

-- UPDATE
update : Msg -> Model -> Model
update msg model =
    case msg of
        ChangeInputText text ->
            { model | inputText = text }
        AddNewTodo desc ->
            { model | todos = Todo.add desc todos }
        ChangeTodoStatus id status
            { model | todos = Todo.changeStatus id status todos }
        EnableEditMode id ->
            { model | todos = Todo.enableEditMode id todos }
        ChangeEditModeText text ->
            { model | todos = Todo.changeEditModeText id text todos }
        ChangeTodoDescription desc ->
            { model | todos = Todo.changeDescription id desc todos }
        DeleteTodo id ->
            { model | todos = Todo.delete id todos }

Since it encapsulates lists, we’ll also expose a method for iterating through the list so that we can render the output.

-- Render a list of todos
viewTodos : Todo.List -> Html.Html Msg
viewTodos todos =
    Html.ul [] Todo.map viewTodo todos

-- Render a single todo
viewTodo : Todo.Todo -> Html.Html Msg
viewTodo todo =
    Html.li []
        [ case todo.mode of
            Todo.View -> viewTodoInViewMode todo
            Todo.Edit text -> viewTodoInEditMode todo text
        ]

-- Render the "view" mode of a todo
viewTodoInViewMode : Todo.Todo -> Html.Html Msg
viewTodoInViewMode todo =
    Html.div []
        [ viewTodoCheckbox todo
        , Html.span [] [ Html.text todo.description ]
        , viewTodoEditButton todo
        , viewTodoDeleteButton todo
        ]

-- Render the "done" checkbox of a todo
-- Also maps changes in the textbox to `ChangeTodoStatus` messages
viewTodoCheckbox : Todo.Todo -> Html.Html Msg
viewTodoCheckbox todo =
    Html.input
        [ Html.Attributes.type_ "checkbox"
        , Html.Attributes.value todo.status
        , Html.Events.onChange (ChangeTodoStatus todo.id)
        ] []

-- Render the "edit" button of a todo
-- Also maps button clicks to `EnableEditMode` messages
viewTodoEditButton : Todo.Todo -> Html.Html Msg
viewTodoEditButton todo =
    Html.a [ Html.Events.onClick (EnableEditMode todo.id)] [ Html.text "Edit" ]

-- Render "delete" button for a todo
-- Also maps button clicks to `DeleteTodo` messages
viewTodoDeleteButton : Todo.Todo -> Html.Html Msg
viewTodoDeleteButton todo =
    Html.a [ Html.Events.onClick (DeleteTodo todo.id)] [ Html.text "Delete" ]

-- Render "edit mode" for a todo
-- Also maps changes in the description (drafted or completed) to `ChangeEditModeText` and `ChangeTodoDescription` messages respectively
viewTodoInEditMode : Todo.Todo -> Todo.Description -> Html.Html Msg
viewTodoInEditMode todo draft =
    Html.form [ Html.Events.onSubmit (ChangeTodoDescription todo.id draft)]
    [ Html.input
        [ Html.Attributes.value draft, Html.Events.onInput ChangeEditModeText ] []
    ]

Notice how we’ve broken the view into several “helper” functions, rather than turning them into separate components/modules. They are still relatively small and easy to understand (once the syntax is gotten used to!)

The Todo module can then expose the relevant functions and data structures:

module Todo exposing (..) -- exposing everything for the sake of this example

import List

-- DATA STRUCTURES
type alias Id = String

type alias Description = String

type alias Status = Bool

type Mode = View | Edit Description

type alias Todo =
    { id : Id
    , description : Description
    , status : Status
    , mode : Mode
    }

type alias List = List.List Todo

-- FUNCTIONS
add : Description -> List -> List

changeStatus : Id -> Status -> List -> List

enableEditMode : Id -> Mode -> List -> List

changeEditModeText : Id -> Description -> List -> List

changeDescription : Id -> Description -> List -> List

delete : Id -> List -> List

map : (Todo -> a) -> List -> List.List a

A brief aside on side effects

There are certain scenarios where we cannot operate with truly pure functions. If I make an HTTP request, I need to wait for the response from the network: this is asynchronous, and hence stateful. If I want to check the current time, that requires checking the system’s clock, which means accessing global state. Same goes for accessing localStorage.

Commands and Subscriptions allow us to abstract away side-effect specific code and treat it as though it were pure. The update and init functions change to now return a pair of Model and Cmd, so that on the next state change the runtime knows which commands it needs to execute.

Elm bakes some top-level support for some actions in this way, such as HTTP requests. Where it isn’t supported, we effectively we create “interfaces” for the side-effecting code (called ports) – similar to how we deal with the repository pattern – then use Subscriptions and Commands to hook it into the event loop of Elm (translating the event responses into Msgs). This is a topic that deserves its own article(s) though, so I’ll leave it at that for now.

Summary

In OOP and component-based design, we organise state and methods into objects/components, which manage fragments of local state and communicate to each other through a public API.

In FP, we define data structures and functions which fully map out the intended state-space (from the top->down), and create modules for managing data types (statelessly).

Helpful Resources

Back to Articles