25 Sep 2015

I recently wrote a navigation app for a hack day, and since it’s written in Elm, and there aren’t enough blog posts about Elm, I thought I’d break down some of the pieces. Here’s a nice juicy one: letting the user search for a location, and turning that into latitude & longitude - ‘geocoding’.

The Spec

  • We need an input box you can type your search into.
  • When you submit, we fetch a list of possible matches from ArcGIS (the company that sponsored the hack day in question, and who have a Geocoding API).
  • We need to display those matches.
  • You can choose one. (A choice which will cause something else to happen. Something that’s outside the scope of this post).

Let’s see how Elm tackles it…

The Implementation

Every Elm app breaks down into a few simple parts:

  • A data structure representing the state of the world.
  • A set of possible events that can occur.
  • An event handler: a function that takes the current state of the world, and one event, and returns the new state of the world, plus any side-effects that must occur.
  • A function that can display the current state of the world.

Let’s break those pieces down:

Model

Our state of the world, our Model, will need to capture the contents of the form, the list of matching places, and the chosen place. And for UX-sweetness we’ll add in a loading-flag, so we can disable the form while we’re fetching results.

In Elm-land we’ll model this as a record. First we’ll need to define the type of a search result, which I’ll call a Place:

type alias Place =
  {address: String
  ,latitude: Float
  ,longitude: Float}

Now we can define the whole app state:

  type alias Model =
    {searchTerm : String
    ,loading : Bool
    ,places : Maybe (Result Error (List Place))
    ,chosenPlace : Maybe Place}

chosenPlace is a Place, of course, but initially no-one’s chosen anything, so we’ll wrap it in a Maybe.

places is the trickiest type to read here. Maybe (Result Error (List Place)) is saying:

  • Maybe We may or may not have…
  • Result Error ... the result of a request, which may be failure, or may be…
  • List Place a list of places.

That’s it. That’s the state of the world, the whole state of the world, and will fully describe the state of the world for the lifetime of the app1.

Action

By convention, Elm describes all events that could occur in the system with an Action type. What are the events that can occur in our app?

  • The user types something, changing our search term.
  • The user hits submit.
  • The internets give us a list of places.
  • The user chooses a place.

That translates into a union type of:

type Action
  = ChangeTerm String
  | Submit
  | PlacesResponse (Result Error (List Place))
  | ChoosePlace Place

For those unfamiliar with the syntax, this is saying that an Action can be any one, and only one of the values that follow. Some, like ChangeTerm carry a payload value of String, some like Submit are just markers.

That’s it. Those are the events, all the events, and Action fully describes every event that can occur for the lifetime of the app2.

update

The next piece we need is an event handler, which we call update. Let’s start with an easy, but incomplete version:

update : Action -> Model -> (Model, Effects Action)
update action model =
  case action of
    ChangeTerm s -> ({model | term <- s}, Effects.none)

This is saying, “if you give me an Action and the current Model of the world, I can process that Action and give you the new Model, plus any side-effects that need to occur (like kicking off an AJAX request).”

In this case, we’re just handling the “user types into the input box” case, to get used to the syntax. We unpack the string that the ChangeTerm action holds, and update the model with it. This case causes no side-effects, so we say that explicity with Effects.none.

The other easy event is “user chooses a place”. Let’s add that into our update function’s case statement:

  case action of
    ...
    ChoosePlace p -> ({model | chosenPlace <- Just p}, Effects.none)
    ...

The next up is submit:

  case action of
    ...
    Submit -> ({model | loading <- True}
              ,Effects.map PlacesResponse (findPlaces term))
    ...

That’s more interesting. We’re kicking off a search. In the model, we set the loading flag. This is purely for UX. Alongside that, for the first time, we have an interesting side-effect. We’re calling findPlaces to create a request, and then using map to wrap the response in a PlacesResponse Action, so that we’ll know know what kind event it is when we come to process it later.

We’ll see how findPlaces is defined later. For now, let’s finish off update by defining how to handle the response:

  case action of
    ...
    PlacesResponse xs -> ({model | places <- Just xs
                                 , loading <- False}
                         , Effects.none)
    ...

When we get a response, switch that loading flag off, and store the places.

That’s it. That’s the business logic, all the business-logic, and fully describes how every event changes the state of the world for the lifetime of the app3.

Pause

This seems like a lot, so let’s take a breather. We have events. We have a state of the world. We have a function that moves the state of the world forward.

It’s nice that the events follow pretty directly from asking, “What can the user do?”

It’s also nice that once we’ve describe how to handle each event, we’re done with business logic.

In the next release of Elm, the compiler will be able to warn you if you miss out any event handlers4, and that will be very nice indeed.

So now we just needed to describe that findPlaces function, and worry about the UI. To do that we’ll need to talk about Effects, JSON-parsing and HTML-rending.

Why don’t you go and make a cup of tea5 and join me for part two.

In the meantime, you can play with the finished hackday app here and read the source code to learn more.

Footnotes

  1. Good luck trying to say something that definitive in an Angular app. 

  2. Good luck trying to say something that definitive in an Angular app. 

  3. Good luck trying to say…oh, you get the point. 

  4. And quickly, at that. Elm’s compiler is impressively fast. 

  5. Make it /properly/ mind. 

comments powered by Disqus