Why Would I Ever Elm?
Joshua HawxwellThis is the blog post version of a talk I gave at NottsJS.
I’ve now been writing Elm for over a year, including in production at work. My only real exposure to front-end frameworks before that was AngularJS which now I think about it is kind of crazy. Before this I’d try to avoid writing too much JavaScript. My attitude has now changed: it is fine deploying complex web applications, but I’d still prefer not to write them in JavaScript.
Throughout this I want to list what I think typed functional programming provides us when writing (but not limited to) front-end applications, with Elm as an example of a language that has those characteristics.
Attributes of typed functional programming
I’m going to list what I think of when trying to define the attributes typed functional programming languages give you over any others. This is not a standard list, hopefully it is not too controversial.
Functions exist as a 1st-class concept. You can define a function; you can pass a function as a parameter to another function; you can have a function that returns functions.
Immutable data. Instead of having some data that gets mutated over the lifetime of the application we have functions that return new data depending on the inputs, and the action of doing so does not modify the input. This is similar to how strings work in most modern languages, but now for lists, dictionaries, etc.
Union types. There are typically nice ways to represent choice with data. So a binary tree can be represented as a type which can be either “a leaf with a value” or “a node with two subtrees”.
Considering JavaScript we can say that we have 1st-class functions. We could also use a package like immutable-js to get immutable data. Union types can be faked easy enough, but it is difficult to enforce correct usage across your codebase in an automatic way.
I’m going to go through each of these and show how it applies to making a typical web application.
Functions make views safe
My main issue with AngularJS (and other frameworks) is writing correct views. Since they are just HTML in a string, or required from a separate file, or some other random syntax. There is no way of knowing whether any of the values you reference actually exist, or whether any of the handlers for events have been defined.
This is true in React.
class HelloMessage extends React.Component {
render() {
return (
<div>
Hello {this.props.name}
</div>
);
}
}
I want to know before I open my web browser that:
- All of the HTML elements and attributes are spelt correctly
- All of the values referenced exist
In the HelloMessage
React component I could easily mispell <div>
, or I could
not pass a name
in the properties where it is used. If I did make a mistake
like that my build would still work and I’d have to open my browser to
check it.
Elm solves these issues by making everything a typed function.
helloMessage : { name : String } -> Html msg
helloMessage model =
Html.div [] [ Html.text ("Hello " ++ model.name) ]
There are functions for each of the HTML elements and attributes, my view is a function that has a type based on the values I reference, and the compiler will fail my build if something doesn’t match. As a bonus I don’t have to write HTML!
name : Model -> Html Msg
name model =
Html.div
[ Attr.class "field"
, Attr.classList [("valid", List.isEmpty model.name.errors)]
]
[ Html.input [ Attr.class "input", Events.onInput SetName ] []
, if List.isEmpty model.name.errors then
Html.text ""
else
Html.div [ Attr.class "help" ]
(List.map Html.text model.name.errors)
]
Since views are just functions, and not some random other syntax mixed with JavaScript, I can easily write complex views without having to remember how to do an if, or apply a function over a list.
Union types for representing states
When writing our complex web applications with pages, users, forms, and decisions, we probably know that we could represent this as a state machine. I’d bet at some point when writing a feature someone wrote a flow chart for part of it too, and then when it came to implement it how did you do it? A few booleans works for most situations, right?
Let’s go through the stages of requesting a user’s new posts. I can think of four states this request could be in:
- The request hasn’t started yet
- The request is in progress, we may want to display something to show that it is loading
- The request succeeded, we can display the new posts
- The request failed, we might want to display an error or retry the request
Here is the “simple” boolean approach to this:
class PostsRequest {
constructor(userId) {
this.posts = []
this.isDone = false
this.isLoading = false
this.isSuccess = false
}
get() {
// do the request and keep all of the booleans correct
}
}
Now in our views we can check isLoading
to show/hide a spinner, or isSuccess
for an error message, but we’d also have to check isDone
or we might show an
error when the request hasn’t even started. We are beginning to see the
complexity of this and it is due to the fact that these 3 booleans can represent
8 different states, double the number we expect to exist. We could use a number,
like an enum, which is how XMLHttpRequest.readyState
works. Then all that you
need to do is remember what 2
means.
In Elm things are much simpler.
type PostsRequest
= Initial
| Loading
| Success (List Post)
| Failure Error
We can create a Union type with four different “tags” (or “constructors”/whatever you want to call them). The really cool thing about this is we can associate data with certain tags, so here we have a list of posts when the request is a success, and an error when it fails.
Also when writing our view we will be forced to handle all of the possible cases by the compiler,
view : Model -> Html msg
view model =
case model.request of
Initial ->
Html.button [ Html.Events.onClick FetchPosts ]
[ Html.text "Get New Posts" ]
Loading ->
Html.div [] [ spinner ]
Success posts ->
Html.ul [] (List.map viewPost posts)
Failure error ->
Html.div [] [ errorToHumanFriendlyDescription error ]
This particular example is of such a common pattern that a package exists to do this.
We could also consider how to deal with form validation using this pattern. In other frameworks we may have something which we can query to see if it has been touched, or is valid. Here is an example from the Angular guide.
<input id="name" name="name" class="form-control"
required minlength="4" appForbiddenName="bob"
[(ngModel)]="hero.name" #name="ngModel" >
<div *ngIf="name.invalid && (name.dirty || name.touched)"
class="alert alert-danger">
<div *ngIf="name.errors.required">
Name is required.
</div>
<div *ngIf="name.errors.minlength">
Name must be at least 4 characters long.
</div>
<div *ngIf="name.errors.forbiddenName">
Name cannot be Bob.
</div>
</div>
If we pretend that we have something similar in Elm we can work backwards to find the type required.
nameInput : Html Msg
nameInput =
Html.input
[ Attr.id "name"
, Attr.name "name"
, Attr.class "form-control"
, Attr.required True
, Events.onInput NameSet
]
nameErrors : Field -> Html msg
nameErrors field =
case field of
Initial ->
Html.text ""
Valid value ->
Html.text ""
Invalid errors ->
Html.div [ Attr.class "alert alert-danger" ]
(List.map displayNameError errors)
displayNameError : Error -> Html msg
displayNameError error =
case error of
Required ->
Html.text "Name is required"
MinLength min ->
Html.text ("Name must be at least " ++ toString min ++ " characters long.")
Forbidden existingName ->
Html.text ("Name cannot be " ++ existing ++ ".")
Given this we can work out what we need Field
to be.
type Error = Required | MinLength Int | Forbidden String
type Field = Initial | Valid String | Invalid (List Errors)
This is only part of the solution, we’d need some function to do the actual
validation too. And if we did start using this the definition of Error
is
probably too specific, it works well for this single input field but not for
others. Luckily there are plenty of packages for validation in Elm that could be
used instead of defining it all ourselves. (You can search
http://package.elm-lang.org/ for them, I like elm-validation).
Immutability makes change easy to manage
For a few years now there has been a move towards thinking about “state management” with packages like Redux or MobX. This is good as trying to keep track of where a value is modifed is difficult. Centralising this in to one place helps and also enables you to easily layer other things like logging on top.
Here is an example showing what this general pattern looks like.
const initialState = { count: 0 }
const next = (message, prevState) => {
switch (message) {
'INC':
return { count: prevState.count + 1 }
'DEC':
return { count: prevState.count - 1 }
}
throw new Error(`next doesn't know how to ${message}`)
}
const state0 = initialState //=> { count: 0 }
const state1 = next('INC', state0) //=> { count: 1 }
const state2 = next('INC', state1) //=> { count: 2 }
const state3 = next('DEC', state2) //=> { count: 1 }
const state4 = next('INC', state3) //=> { count: 2 }
// ...and so on
We have an initial state; a function that takes a message representing something to do or that has happened, and the previous state, and that returns a new state; and then below we just apply that function over the previous states.
This is the same idea as is used in Elm. But it is necessary in Elm because there are no “variables”. We need a way to modify the immutable data that makes up our application’s model of the world, a function that takes a message and the previous value and then gives us the new one is perfect. The pattern is referred to as The Elm Architecture in the Elm world. Here is the previous example in Elm, for example:
initial : Int
initial = 0
type Msg = Increment | Decrement
update : Msg -> Int -> Int
update msg prevCount =
case msg of
Increment ->
prevCount + 1
Decrement ->
prevCount - 1
As before I define an initial state, here called initial
, and a function to
work out what is next, here update
. We also get the benefit of using a union
type for the message, so the compiler will enforce that only these two things
will ever happen. I don’t need a default case that throws an error!
Closing
I think that these ideas are powerful, and so do others, that’s why there are attempts to bring them to JavaScript in packages we can easily include in existing applications. I want to see more of that, in particular a good way of doing union types with exhaustion checks.