The Good, the Bad, and the Elmy
I decided to begin developing a web app to keep track of groups in some of my classes. Ultimately I’d like to be able to automate project milestone check-ins for these groups (is your git repo up, can I clone it and run docker-compose, etc.) but the first part of that is just getting the groups and members in a database. Looking through the long list of things I would like to play with, I decided on developing the front end in Elm with Bulma for the styling. I’ll discuss back end development in upcoming posts, but in this post I really wanted to comment on my first experiences with Elm. You can find the full source here.
There is plenty of good beginner documentation on Elm and I encourage you to read it (even if you don’t heed my advise, eventually the compiler itself will recommend you read it). This post isn’t meant to cover beginner topics so much as try to capture a developer experience.
The Good
Elm is a functional language (don’t forget what the first three letters in
functional programming are) that uses a message passing model to handle
updates. Having thoroughly enjoyed working in Erlang and Clojure in the past
these features put Elm on my shortlist of things to try. To make things even
better Elm is strongly typed (more on that later) and has modern compiler
warnings that actually help you figure out what’s going on. I cut my teeth in
the age of SYNTAX ERROR
so I think I’ve had enough terse compiler messages to
last me a lifetime.
Elm focuses on defining the data and how things interact with it. The Elm
architecture expresses this as the Model (should be familiar to web devs) and
it really amounts to some custom types and an init
function:
-- MODEL
type alias Group =
{ id : Int
, name : String
, gitUrl : String
, members : List String
}
type alias User =
{ ucid : String
, google_name : String
}
-- Custom type so that modal is either active with data or inactive
type EditModal
= Active
{ group : Group
, searchText : String
, buttonText : String
, title : String
, onClick : Group -> Msg
}
| Inactive
type alias Model = (1)
{ groups : List Group
, users : List User
, editModal : EditModal
, notification : String
}
init : () -> (Model, Cmd Msg) (2)
init _ =
({ groups = []
, users = []
, editModal = Inactive
, notification = ""
}, batch [getUsers, getGroups])
1 | This is the heart of it. groups , users , editModal , and notification
are all the state that this web app has. |
2 | The init function is a function just like any other, it gets things ready
at the start. |
An update function handles incoming messages. It’s kind of like Grand Central
Station with trains coming in and out and some tasks being performed on new
arrivals. By far the coolest thing about it is that you must define your
messages as well as the data they carry with them. Just by glancing at an app’s
Msg
declaration you can get a good feel for what that app is doing. Here’s
mine:
type Msg
= GotGroups (Result Http.Error (List Group))
| GotUsers (Result Http.Error (List User))
| AddGroupButton
| EditGroupButton Group
| DeleteGroupButton Int
| CancelEditButton
| CreateGroup Group
| UpdateGroup Group
| ReloadGroups (Result Http.Error ())
| AddMember String
| SearchInput String
| NameInput String
| GitInput String
You can see what buttons can be pressed (Add
, Edit
, Delete
, Cancel
),
what data will be sent to the back end API (CreateGroup
, UpdateGroup
), and
you can see the Msg
used to handle the responses (GotGroups
and
Got Users
). In Elm it’s typical to update the model every time a text input
is changed, so you will also see SearchInput
, NameInput
, and GitInput
.
That really comes in handy later when I decided to implement something a little
more atypical. It should be noted that I’m not showing the actual update
function, because at this point I just want to try to give the feeling of the
approach that Elm takes.
The last part of the basic Elm architecture (which interestingly was established organically after the language itself was created) is the View. Once again, this should be familiar to web devs as the HTML that you’re actual spitting out for a user to view. Elm has an HTML package (similar to Hiccup if you’ve worked in ClojureScript) that makes generating HTML easy. You’re going to lean heavily on functions and Lists (as you would in any FP) to create you’re HTML. Here’s a snippet that creates a table of Groups:
rowControls : Group -> Html Msg (1)
rowControls group =
div [ class "field", class "is-grouped" ]
[ p [ class "control" ]
[ a [ class "button", class "is-link", onClick (EditGroupButton group) ]
[ text "Edit" ]
]
, p [ class "control" ]
[ a [ class "button", class "is-danger", onClick (DeleteGroupButton group.id) ]
[ text "Delete" ]
]
]
rowItem : Group -> Html Msg (2)
rowItem group =
tr []
[ td [] [ text (String.fromInt group.id) ]
, td [] [ text group.name ]
, td [] [ text group.gitUrl ]
, td [] (List.map (\ucid -> div [] [ text ucid ]) group.members)
, td [] [ rowControls group ]
]
groupTable : List Group -> Html Msg (3)
groupTable groups =
table [class "table", class "is-hoverable", class "is-fullwidth"]
[ thead []
[ tr []
[ th [] [ text "ID" ]
, th [] [ text "Name" ]
, th [] [ text "Git URL" ]
, th [] [ text "Members" ]
, th [] [ text "Actions" ]
]
]
, tbody [] (List.map rowItem groups) (4)
]
1 | these are the edit/delete buttons at the end of a row |
2 | this is the actual row |
3 | this is the table with headers and all the rows attached |
4 | rows are attached here |
One of the things that initially confused me about Elm was understanding how
everything is tied together. The application defines a main
function, of
which there are different patterns depending on what you’re application needs to
be able to do. The most basic that could meet my needs was Browser.element
.
The main
function brokers the transfer of messages and model updates between
components (functions registered in a record when your main function is
declared). The data transferred between these functions looks like this:
As you can see, init
simply gives the initial state of the model, as well as
any command (Cmd Msg
) you want to run at startup. In our case we run both the
getUsers
and getGroups
commands (which are themselves simply functions that
return messages):
init : () -> (Model, Cmd Msg)
init _ =
({ groups = []
, users = []
, editModal = Inactive
, notification = ""
}, batch [getUsers, getGroups]) (1)
1 | Platform.command.batch simply runs multiple commands in parallel |
Messages are passed (along with the model) to update
as they come in and
update
returns and updated model as well as any commands that need to be run.
update
is the only way your application moves through state changes and as
such update
really seems to define exactly what your web app does. For me
at least, it took some mental gymnastics to figure out exactly how I wanted to
do things. For example updating a group, which I would typically conceptualize
as a single action, really involved at least three messages:
Messages can carry data or even other messages (yo dawg) and not only do they
create an airtight concurrency system, their strict typing also ensures that
your app never reaches an unanticipated state. All messages need to have a case
in update
. What happens if I forget to implement a branch for ReloadGroups
?
It won’t compile. What happens if ReloadGroups
gets a result it didn’t
expect? Can’t happen, all possibilities for the type must be handled or it
won’t compile. Notice a trend?
The Bad
Great! So we have a front end system where we can’t possibly shoot ourselves in the foot, right? Wrong. Fear not my dear reader, in my infinite wisdom I still managed to screw up a lot of things.
The first bit of advice that I didn’t take until after I adopted experience and
pain as a teacher is this: don’t stub out your code. This isn’t your
typical JavaScript dev experience,
where you stop every 90 seconds to make sure you didn’t mess something up. If
you’re going to implement something, do it. Don’t make a little pass through
branch in update
that returns an unchanged model, it’ll only bite you in the
butt later when you’re wondering why your app compiles but doesn’t have that
particular bit of functionality.
Don’t trust the compiler to figure out how to organize arguments. This is functional programming and you’re going to be building functions of functions of functions of functions… Elm has a nice syntax that doesn’t require the Long Irritating Strings of Parenthesis that you may be used to (or even love). You can and will still use them to help the compiler make sense of what you mean. A quick REPL session will demonstrate:
ryan@wsl2:/home/ryan$ elm repl
---- Elm 0.19.1 ----------------------------------------------------------------
Say :help for help and :exit to exit! More at <https://elm-lang.org/0.19.1/repl>
--------------------------------------------------------------------------------
> import String
> import Html
> Html.div [] [ Html.text String.fromInt 42 ]
-- TYPE MISMATCH ---------------------------------------------------------- REPL
The 1st argument to `text` is not what I expect:
5| Html.div [] [ Html.text String.fromInt 42 ]
^^^^^^^^^^^^^^
This `fromInt` value is a:
Int -> String.String
But `text` needs the 1st argument to be:
String.String
-- TOO MANY ARGS ---------------------------------------------------------- REPL
The `text` function expects 1 argument, but it got 2 instead.
5| Html.div [] [ Html.text String.fromInt 42 ]
^^^^^^^^^
Are there any missing commas? Or missing parentheses?
> Html.div [] [ Html.text (String.fromInt 42) ]
<internals> : Html.Html msg
So while the compiler is smart, it isn’t that smart. Which frankly is good, because I’ve had my fill of things that are smart or magical. Be explicit when needed and don’t fear the parenthesis.
While we’re talking about the syntax, let’s
[ talk
, about
, the
, syntax
]
I’m not going to lie, if you’re not used to it and you’re coming from a Rust back end (as I was) it’s a lot like putting your brain into a wood chipper. The best advice I can give you is this: try your best, the compiler is pretty good and making sense of crappy indentation. When it can’t figure it out it will tell you and even give you a code sample. The more examples you read the more natural it becomes, but at first it’s unnerving.
The Elmy
There are a few things that just seemed to happen to me as I was writing code in Elm. First and foremost, I would write my way into a situation that I didn’t think should even be possible. For example when I was developing the modal dialog that allows the user to edit/update a group I found myself guarding against all sorts of possibilities that the data isn’t what it should be. That was mostly because the data doesn’t matter (or make sense) if the dialog isn’t active. With Elm, it turned out to be easier to store the modal data in a custom type that either is active with valid data or isn’t active. Take a look:
type EditModal
= Active
{ group : Group
, searchText : String
, buttonText : String
, title : String
, onClick : Group -> Msg
}
| Inactive
This way the compile makes sure I don’t have to constantly double-check.
I also ended up needing a custom autocompletion component that turned out to be pretty trivial to implement. Here’s the problem: I may have hundreds of students in a semester and they are all identified by their UCID. How do I select one UCID from the hundreds when I don’t have them memorized? The best solution I could come up with was to create a drop down that displayed up to five students that partially matched what I had typed into a search box. In Elm I was already capturing every keystroke in that box to update the model all I had to do was filter a list of possibilities and display them when there were five or less possibilities. It was way easier than I expected it to be:
-- provides HTML for a link within a dropdown menu
dropLink : User -> Html Msg
dropLink user =
a [ class "dropdown-item", onClick (AddMember user.ucid) ]
[ text (user.google_name ++ " (" ++ user.ucid ++ ")") ]
-- determines if a User should be displayed in the dropdown list
matchUser : User -> String -> Bool
matchUser user searchText =
(String.contains searchText (String.toLower user.ucid))
|| (String.contains searchText (String.toLower user.google_name))
-- filters a User list down to which ones should be shown
filterUsers : List User -> String -> List User
filterUsers users searchText =
List.filter (\u -> (matchUser u (String.toLower searchText))) users
-- provides HTML for a live-updating, searchable dropdown selector of users
autoDrop : List User -> String -> Html Msg
autoDrop users searchText =
let
showUsers =
filterUsers (log "autoDrop: users" users) (log "autoDrop: searchText" searchText)
active =
(List.length showUsers <= 5)
in
div [ classList [ ("dropdown", True), ("is-active", active) ] ]
[ div [ class "dropdown-trigger" ]
[ div [ class "field" ]
[ p [ class "control", class "is-expanded", class "has-icons-right" ]
[ input
[ class "input"
, type_ "search"
, placeholder "Search..."
, value searchText
, onInput SearchInput
]
[]
, span [ class "icon", class "is-small", class "is-right"]
[ i [ class "fas", class "fa-search" ] []]
]
]
]
, div [ class "dropdown-menu" ]
[ div [ class "dropdown-content" ]
(List.map dropLink showUsers)
]
]
To wrap up, if you haven’t given Elm a shot, I highly recommend it. It forces/allows you to keep your mind on the problem you are solving. Its pedantry is a welcome counterpoint to the everything is mutable swamp that is JavaScript. Apologies to Mr. Eich.