Lessons learned from my first 10k LOC in Elm

rametta profile image Jason ・5 min read

I have been working on a personal project of mine for the last couple of months that has the frontend written in Elm. So far everything is going great and the project is around 10k lines of code.

I have noticed a few reoccurring patterns of mine that I have learned so far and want to share, so here are 5 things I have learned:

1. Decoding Empty Lists & Strings

When I first started the project, I had a lot of types with fields declared like this:

type alias Something =
  { name : Maybe String
  , stuff : Maybe (List String)

and the way I was decoding these values from my API was like this:

import Json.Decode as D
import Json.Decode.Pipeline exposing (optional)

D.succeed Something
  |> optional "name" (D.nullable D.string) Nothing
  |> optional "stuff" (D.nullable (D.list D.string)) Nothing

This was fine at first, but I noticed that after awhile I was repeating logic inside my update functions to check if the name field was an empty string, or if the stuff field was an empty list, and if it was, change the value to Nothing instead of Just "" or Just [].

So after awhile, I thought it would be more efficient to check for empty strings and empty lists during the decoding phase, so I came up with two decoder helper functions, decodeNonEmptyString and decodeNonEmptyList:

decodeNonEmptyString : D.Decoder (Maybe String)
decodeNonEmptyString =
  D.andThen (notEmptyString >> D.succeed) D.string

decodeNonEmptyList : D.Decoder a -> D.Decoder (Maybe (List a))
decodeNonEmptyList l =
  D.andThen (notEmptyList >> D.succeed) (D.maybe (D.list l))

notEmptyList : Maybe (List a) -> Maybe (List a)
notEmptyList =
  Maybe.andThen (\l ->
    if List.isEmpty l then
      Just l

notEmptyString : String -> Maybe String
notEmptyString s =
  if String.isEmpty s then
      Just a

So now my original decoders use these new utilities like this:

D.succeed Something
  |> optional "name" decodeNonEmptyString Nothing
  |> optional "stuff" (decodeNonEmptyList D.string) Nothing

And I don't need to check for emptiness anymore in my update functions 🎉

2. Flatter pattern matching

This one might seem really obvious, but took me awhile to realize that doing this results in flatter pattern matching functions.

If I have a type like RD.WebData (Maybe (List Person)), the way I original matched on it was like this:

case response of
  RD.NotAsked -> div [] [text "Not Asked"]
  RD.Loading -> div [] [text "Loading"]
  RD.Failure e -> div [] [text "Error"]
  RD.Success maybeData ->
    case maybeData of
      Nothing -> div [] [text "No data"]
      Just data -> div [] [text "Data!"]

But I realized that I could flatten this into one case statement instead of 2 separate ones like this:

case response of
  RD.NotAsked -> div [] [text "Not Asked"]
  RD.Loading -> div [] [text "Loading"]
  RD.Failure e -> div [] [text "Error"]
  RD.Success Nothing -> div [] [text "No data"]
  RD.Success (Just data) -> div [] [text "Data!"]

See? Much nicer!

3. Select Wrapper

This is one of the first abstractions I made when I started because I wanted an easy reusable way to use select [] [] and found that the native one in Elm is not as nice as I hoped.

So I wrote a small wrapper called viewSelect:

viewSelect : Bool -> (String -> msg) -> List Option -> Html msg
viewSelect disabled changeMsg options =
    [ onChange changeMsg
    , Html.Styled.Attributes.disabled disabled
      (\opt ->
          [ value opt.value
          , Html.Styled.Attributes.selected opt.selected
          , Html.Styled.Attributes.disabled opt.disabled
          [ text opt.text ]

type alias Option =
  { text : String
  , value : String
  , selected : Bool
  , disabled : Bool

onChange : (String -> msg) -> Html.Styled.Attribute msg
onChange tagger =
  on "change" (D.map tagger Html.Styled.Events.targetValue)

Now I use viewSelect instead of the native select and I just need to pass in a list of Option instead. Every time a user changes the select box, the changeMsg fires with the new value.

4. ChangeField Pattern

In my app, I have a lot of forms, and every form needs to modify some field inside my model. The way I originally started doing this was:

type Msg
  = ChangeName String
  | ChangeAge String
  | ChangeHeight String
  | ChangeWeight String

update msg model =
  case msg of
    ChangeName name -> ({ model | name = name }, Cmd.none)
    ChangeAge age -> ({ model | age = age }, Cmd.none)
    ChangeHeight height -> ({ model | height = height }, Cmd.none)
    ChangeWeight weight -> ({ model | weight = weight }, Cmd.none)

This was also fine at first, but my app has A LOT of forms and this was not scaling well. So instead of creating a new Msg type for every field, I created one Msg type for updating any field. And it works like this:

type Msg = ChangeField (String -> Person -> Person) String

update msg model =
  case msg of
    ChangeField setter content -> (setter content model, Cmd.none)

Now every time a field needs to change, instead of calling ChangeName "Jason", I can now call ChangeField setName "Jason" where setName equals:

setName : String -> Person -> Person
setName content person =
    { person | name = content }

This allows me to create individual setter functions for each field and do any data manipulations from String into any type I need.

It simplifies my update function by reducing the amount of Msg types it can match on, and it nicely separates out my data manipulation logic for each field.

5. Posix Wrappers

This one is pretty simple really. I use Elm's Time.Posix type a lot around my app, so I needed functions to format, manipulate and display Posix in my UI. I came up with a few helpers for this and call it my HumanTime module:

humanDateTime : Zone -> Posix -> String
humanDateTime z p =
  humanDate z p ++ " " ++ humanTime z p

humanTime : Zone -> Posix -> String
humanTime z p =
  humanHour z p ++ ":" ++ humanMinute z p

humanDate : Zone -> Posix -> String
humanDate z p =
  humanMonth z p ++ " " ++ humanDay z p ++ ", " ++ humanYear z p

humanYear : Zone -> Posix -> String
humanYear z =
  toYear z >> String.fromInt

humanDay : Zone -> Posix -> String
humanDay z =
  toDay z >> String.fromInt

humanHour : Zone -> Posix -> String
humanHour z =
  toHour z >> String.fromInt

humanMinute : Zone -> Posix -> String
humanMinute z =
  toMinute z
      >> (\m ->
            if m > 9 then
              String.fromInt m
              "0" ++ String.fromInt m

humanMonth : Zone -> Posix -> String
humanMonth z p =
  case toMonth z p of
    Jan -> "Jan"
    Feb -> "Feb"
    Mar -> "Mar"
    Apr -> "Apr"
    May -> "May"
    Jun -> "Jun"
    Jul -> "Jul"
    Aug -> "Aug"
    Sep -> "Sep"
    Oct -> "Oct"
    Nov -> "Nov"
    Dec -> "Dec"

And that is it so far! I am sure I will learn even more in the next 10k LOC that I write and might even figure out better ways of doing these things that I just mentioned.

Elm is an amazing tool for creating frontends and I can't ever see myself going back to React or any other non-functional frameworks for personal projects.

If you liked this article, be sure to follow me on DEV to see whenever I post new articles in your feed ✨ or follow me on twitter!

Posted on Oct 24 '19 by:

rametta profile



Software Developer in Montreal, Canada.


markdown guide

Two tips for the Select wrapper:

  • Since only one option can be selected at a time, you can model that by having a selected: Maybe option argument for the whole Select, instead of setting it per option.
  • With some tricks you can make Selects work with any type, not just Strings!

Here's a demo :) ellie-app.com/7253Q8THR2xa1


That's great, thank you Simon!


Doing a personal project myself and Elm is just a breeze to work with. So elegant.


Very nice article! I resonate with much of what you have found to be true.