Quickstart Elm 0.19, part 17
Adding side effects to a todo app in Elm 0.19
By: Ajdin Imsirovic 06 November 2019
Elm is a purely functional programming language for building web app frontends. In this article, we’ll see some practical examples of what that means, through the lens of side effects.
Note: Examples in this article use Elm 0.19.
Why use a functional language on the front-end?
Elm makes it virtually impossible to introduce errors into our code.
We’ve already seen Elm’s helpful, non-cryptic compile-time errors. Combined with strong type system, introducing errors into our production code is on the verge of being impossible.
Error-driven development, pre-compile time
After you have worked with Elm for some time, you start to feel as if Elm is constantly narrowing the window of opportunity for errors to appear in your code, from the way it is structured to the way you work with it. You can feel that conscious effort was placed on making it less probable for errors to occur.
As Elm is built around the paradigm of functional programming, it works with pure functions. Pure functions are functions that have no side effects. In other words, pure functions have no state. They accept parameters, and they return a value. That is all they do!
They will not go out and make an HTTP request. They will not mutate variables. They will not, in any way, change the state of the world. They simply return a value. This brings us to an interesting point - as long as we provide the same values to a function in Elm, it will return the same result.
Another wonderful benefit of pure functions in Elm is that you can be certain that all the changes you make in your code are local. Changing a piece of code in your app will not cause some other piece of code in your application to stop working.
Also, when working with third-party libraries, you cannot be sure that they have adhered to functional style purity. Compare that to Elm, which enforces nothing else but pure functions - thus you know that Elm packages are rock-solid.
Some other frameworks, such as Angular 2+, have adopted a similar approach:
- Optionally strongly typed (using Typescript)
- Compiler throwing errors and preventing compilation until they are resolved
There is one caveat to this talk of pure functions. You are probably aware that a fully stateless application would be pointless.
The ingenuity of Elm lies in the fact that it has a very strict way of dealing with updates to our application. The takeaway from this is that not only does Elm enforce the functional programming paradigm by forcing us to use pure functions, but it also narrows down ways to deal with the outside world.
Dealing with Randomness in Elm
As mentioned before, values in Elm are immutable. The functions are pure; they allow for no side effects. When a specific value goes in, a function will operate on it and always return another specific value.
This poses a problem — just how do we deal with randomness? For example, how do we generate random numbers? Or, for that matter, how do we perform anything that involves a side effect?
We do that by having functions return commands.
Commands in Elm
With a command, you are telling the Elm runtime to do something you are not allowed to do, since doing it would break the concept of guarantees. Thus, a command that you return from a function is just a static, immutable value. What does this value do? It just names the desired result. It does not tell Elm how to do it. It is just a name for one or more things that need to be done by the Elm runtime.
For example, since the concept of guarantees says that for every input into a pure function, we should receive the same kind of output, we cannot have a function return a random number, since doing so would break the concept of guarantees. In order to make the preceding scenario possible, we need to send a command to the Elm runtime, asking it to give us a random number.
Thus, once the Elm runtime receives a command, such as a request for a random number, it will return a message. Then we can use that message in the update function.
The goal of this very basic introduction to commands in Elm is that there are no ambiguities about the new code that we will introduce, or, put differently, that we have at knowledge about what each piece of code does. Next, we’ll look at subscriptions, and how they fit into the Elm architecture.
Subscriptions in Elm
Commands allow us to tell the Elm runtime to do random things without breaking Elm’s guarantees.
However, let’s say we want the Elm runtime to tell us when some changes happen in the outside world (that is, anything that we cannot directly control in our app). We cannot control the changes in the outside world with commands — because we are not the source of these changes.
That’s why subscriptions exist. They allow us to listen for things such as mouse movements, keyboard presses, time changes, and so on.
With commands, we tell the Elm runtime to do random things; with subscriptions, the Elm runtime tells us of random things being done.
For example, we want to track when a keyboard button is pressed. We’ll have our app subscribe to those keyboard presses. Once that specific keyboard button is pressed,** the Elm runtime will send a message, and **in our update function, we specify how our app should behave when such a message is received.
Now that we are familiar with commands and subscriptions in Elm, we can look at how they can be used to extend our current concept of the Elm architecture.
Improving the Todo App by adding Effects
Let’s first discuss what sections of the app from the previous article we will update.
We’ll ditch the sandbox
we used in previous articles.
import Browser exposing (sandbox)
Instead, we’ll use element
from the Browser module:
import Browser exposing (element)
Because we’re using Browser.element, we had to update the update
function.
The update function is mostly the same as before but now instead of just returning the model, we now return a tuple
containing the new model value and a command
which can perform side effects.
We don’t need to do any side effects, so we’ve just added Cmd.none
as the command for each returning value of the case expression.
Since Elm is a pure functional programming language, the only way you can perform side effects is by using commands and subscriptions.
You’ll see how they work later. Just think of commands as a way of asking for some side effect to happen and think of subscriptions as a way of listening or subscribing to the result of some side effect.
Commands get returned from the update function and the resulting values produced from subscriptions get passed as a message to the update function.
Update function’s signature
The previous app’s update
function signature looked like this:
update : Msg -> Model -> Model -- to be updated
update msg model =
Here’s the updated version:
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
Pattern-matching for UpdateText
Let’s look at the first pattern-match in the old update
function:
case msg of
UpdateText newText ->
{ model | text = newText } -- to be updated with Cmd.none
Here’s the update:
case msg of
UpdateText newText ->
( { model | text = newText }, Cmd.none )
Pattern-matching for AddTodo
Next, here’s the old AddTodo
message:
AddTodo ->
{ model | text = "", todos = model.todos ++ [ model.text ] }
And here’s the updated one:
AddTodo ->
( { model | text = "", todos = model.todos ++ [ model.text ] }
, Cmd.none
)
Pattern-matching for RemoveTodo
Previously, RemoveTodo was handled like this:
RemoveTodo index ->
let
beforeTodos =
List.take index model.todos
afterTodos =
List.drop (index + 1) model.todos
newTodos =
beforeTodos ++ afterTodos
in
{ model | todos = newTodos } -- to be updated with Cmd.none
Now, we’re handling it like this:
RemoveTodo index ->
let
beforeTodos =
List.take index model.todos
afterTodos =
List.drop (index + 1) model.todos
newTodos =
beforeTodos ++ afterTodos
in
( { model | todos = newTodos }, Cmd.none )
Pattern-matching for Edit message
Here’s the old Edit
case:
Edit index todoText ->
{ model | editing = Just { index = index, text = todoText } }
Here’s the new Edit
case:
Edit index todoText ->
( { model | editing = Just { index = index, text = todoText } }
, Cmd.none
)
Pattern-matching for EditSave message
The old EditSave looked like this:
EditSave index todoText ->
let
newTodos =
List.indexedMap
(\i todo ->
if i == index then
todoText
else
todo
)
model.todos
in
{ model | editing = Nothing, todos = newTodos }
Here’s the updated version:
EditSave index todoText ->
let
newTodos =
List.indexedMap
(\i todo ->
if i == index then
todoText
else
todo
)
model.todos
in
( { model | editing = Nothing, todos = newTodos }
, Cmd.none
)
Finally, we’ll take care of subscriptions
, init
and main
:
-- We don't need subscriptions, so we're just going to have the subscription
-- function return Sub.none, which indicates we have no subscriptions.
-- I'll explain subscriptions more in the future when we use them, so don't
-- worry about them right now.
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
init : () -> ( Model, Cmd Msg )
init flags =
( { text = ""
, todos = [ "Laundry", "Dishes" ]
, editing = Nothing
}
, Cmd.none
)
main : Program () Model Msg
main =
-- We are now using element instead of sandbox, which takes
-- a record with the properties: init, view, update, and subscriptions.
-- The init property is similar to the init property in sandbox
-- except that it takes a function that takes in flags and return a tuple of
-- type ( Model, Cmd Msg ). The Cmd Msg is useful for if you want to
-- perform any side effects in the beginning of the program. You usually
-- don't need to perform any side effects, so you just put the value Cmd.none
-- as the command value whenever you don't need to do any commands.
element
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
Here’s the app’s update on Ellie:
Next, we’ll improve the record we pass to the init function.
Improving the init function
This is the short-hand version of what we had before. The model was:
{ text = "", todos = ["Laundry", "Dishes"], editing = Nothing }
We can also represent this value like:
Model "" [ "Laundry", "Dishes" ], editing = Nothing }
Whenever we make a type alias that’s a record, like Model, we can use Model as a constructor function that returns a Model record.
Since we defined the Model type alias like this:
type alias Model =
{ text : String
, todos : List String
, editing : Maybe TodoEdit
}
… (Model "" [ "Laundry", "Dishes" ] Nothing)
will make the first argument the text property since that is first in the type alias declaration. The second argument will be the todos
property, and the third argument will be the editing
property.
init : () -> ( Model, Cmd Msg )
init flags =
( Model "" [ "Laundry", "Dishes" ] Nothing
, Cmd.none
)
In the next article, we’ll add local storage to our Elm 0.19 app.