The Good, the Bad, and the Elmy

code

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:

mvu

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:

message

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.