Improving Elm Routing with Union Types

Make Your Code More Expressive

billperegoy
im-becoming-functional
3 min readJun 6, 2017

--

Looking Back at a Single Page Web App Example

A while back, I wrote a blog post demonstrating how to build a single page web app with Elm. I was looking at this code again recently and I was not very satisfied with one aspect of it. My router was very simple. It matched only a few route types and I did this by splitting the URL hash on the ‘/’ character and pattern matching against the array of path components. I ended up with this code in my view.

pageBody : Model -> Html Msg
pageBody model =
let
routePath =
fromUrlHash model.currentRoute.hash
in
case routePath of
[] ->
homePage
[ "home" ] ->
homePage
[ "about" ] ->
aboutPage
[ "users" ] ->
usersPage model
[ "users", userId ] ->
userPage model userId
[ "users", userId, "hobbies" ] ->
hobbiesPage model userId
_ ->
notFoundPage

What I’m trying to do here is pretty simple. I want a route of “/users” to render the all users page, a route if “/users/:id” to render an individual users page, etc.

While this code is perfectly functional, I see a few problems here.

  1. The route parsing logic is intertwined with the view logic. This violates the single responsibility principle.
  2. There is no way to test the route parsing logic without also testing the view.
  3. The code just isn’t very expressive. Pattern matching against a list of strings doesn’t really declare the intent of this code.

Adding Expressiveness with Elm Union Types

Generally, when I find myself with data types and code that doesn’t seem very expressive, I begin to think about using Elm union types. Union types give us the ability to express higher-level concepts without relying on primitive data types.

In this case, it made sense to think of the route type as a union type instead of a list of strings. So, I changed the route type definition from.

type alias RoutePath =
List String

To this:

type RoutePath
= DefaultRoute
| HomeRoute
| AboutRoute
| UsersRoute
| UserRoute String
| HobbiesRoute String
| NotFoundRoute

This clearly communicates the possible routes in a very self-documenting way. I then wrote a function that converted the route hash into this type.

fromUrlHash : String -> RoutePath
fromUrlHash urlHash =
let
hashList =
urlHash |> String.split "/" |> drop 1
in
case hashList of
[] ->
DefaultRoute
[ "home" ] ->
HomeRoute
[ "about" ] ->
AboutRoute
[ "users" ] ->
UsersRoute
[ "users", userId ] ->
UserRoute userId
[ "users", userId, "hobbies" ] ->
HobbiesRoute userId
_ ->
NotFoundRoute

You’ll note that this code uses the exact same case statement as the previous example. But instead of producing a view, we produce a result of the the type RoutePath.

This is a small but powerful change. We now have a simple standalone function that can be tested in isolation without testing the entire view.

With this change, our view fragment then looks like this.

pageBody : Model -> Html Msg
pageBody model =
let
routePath =
fromUrlHash model.currentRoute.hash
in
case routePath of
DefaultRoute ->
homePage
HomeRoute ->
homePage
AboutRoute ->
aboutPage
UsersRoute ->
usersPage model
UserRoute userId ->
userPage model userId
HobbiesRoute userId ->
hobbiesPage model userId
NotFoundRoute ->
notFoundPage

This looks familiar in that it’s similar to the previous case stement. But instead of switching on a list of strings we switch on the new union type. This has the advantage of being easier to read. Plus, we get compiler support. If we misspell one of the route names, the compiler will flag this error. In the previous case, a typo would just result in an unintended route to the notFoundPage.

Lessons Learned

Using Elm union types in this case actually resulted in more code. Is that a good thing? In this case, I’d vote yes as it gained us these advantages.

  1. We have a more testable standalone route parsing function.
  2. We now have compiler support for errors in the route name. The compiler will flag these errors, removing one possibility of functional errors.
  3. Our view code is much more expressive and easier to read. The low level clutter of the URL parser is moved to one isolated function.

You can find an example of this code at https://github.com/billperegoy/elm-spa/blob/master/src/Main.elm. I’m hoping this will spur some ideas of your own code by moving from overuse of primitives to the expressive union types we get from Elm.

--

--

billperegoy
im-becoming-functional

Polyglot programmer exploring the possibilities of functional programming.