Categories
Front-End Software

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

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