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 model
s:
-- 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).