The ELM Confusion
12 February 2017
Today I think I may (possibly) have understood something about ELM. Before we look at some code, here is the 5sec intro into ELM: ELM is essentially a Haskell that runs in the browser. It makes an enormous effort to have a friendly and helpful compiler. And it succeeds at it! It promises zero runtime errors, has a strong type system and comes with a batteries-included architecture that is supposed to result in easy to maintain web apps.
I am currently experimenting with porting a few Angular components over to ELM to see how it compares. One interesting aspect is that I get data from a remote system. From a previous experience, I had a hunch that 'async' code in ELM quickly got me confused with
Msg (the last one is an app-specific custom type).
But this time all went smooth. Here is the code to read some data from an endpoint and parsing the resulting JSON:
getData : Cmd Msg getData = let url = "http://localhost:3001/people" request = Http.get url Data.decode in Http.send NewData request
The focus point of this post will be that function and the inconspicuous NewData.NewData is a custom union type that represents what messages my application will react to:
type Msg = MorePeople | NewData (Result Http.Error Response)
The above code was just happily living in the initial App.elm file, which was fine for the beginning.
The challenge I set myself was to separate the concern "getting data from a remote system" from the general flow of my application, which is encoded in an update function and the corresponding Msg type.
Naive as I was, I just moved the
getData function over to the Data.elm file (a poor name in hindsight) and fixed some imports. Sadly, the compiler was not happy about this:
OK. I can understand that. I've obviously not imported
Msg and by extension
NewData. Hmm... That type lives in the App module, which imports the Data module, which would import the App module, which imports… argh. Circular dependency.
While still on the naive path, I thought I could just extract
Msg into a parameter of the function. That move made sense to me: the Data module should just retrieve the data and not worry about how the main App will continue to process it. So this is what I thought I'd need:
getData : msg -> Cmd msg getData msg = let url = "http://localhost:3001/people" request = Http.get url decode in Http.send msg request
As you can see, the function now takes a parameter and should be invoked as
Data.getData NewData and produce a
Cmd Msg. Oh, how nice that would have been. The compiler and I had a slight disagreement about the correctness of this code:
send expects a function that takes a
Result with an
Http.Error and something (that is the 'a') over to
msg? That's odd. I had to double check that I had not deleted more than I wanted since all I had really done was introducing a new parameter.
Understanding and reasoning
It took me a while to grok what was going on here. I kept staring at it and thinking "where is that function coming from?". My first step was to just take what the compiler was saying for granted and changing the signature to match the error.
getData : (Result Http.Error a -> msg) -> Cmd msg getData msg = let url = "http://localhost:3001/people" request = Http.get url decode in Http.send msg request
This lead to an error about send not really liking a
Result Http.Error a and preferring
Result Http.Error Response. Replacing that
a with a
Response made everything snap into motion. Bare with me.
msg parameter of the above function definition is supposed to be the message type that I wanted to abstract away from. The one variant that I had there initially is
NewData (Result Http.Error Response). That contains all the elements – though slightly rearranged – as the function that is passed as the first parameter to
getData. And that is it!
NewData is not an enum variant like in Java or Rust, it's a type constructor! It is a function that takes a
Result Http.Error Response and produces a new instance of
Msg. Spelling that out in Elm is
(Result Http.Error Response -> Msg). To not depend on the type itself, it is extracted both in the function and the resulting
This post might some long-winded followed by a very quick conclusion. Keep in mind, it took me a few hours and attempts to get to the point where it all
clicked. Then it was super easy. In hindsight, if I had just followed the compiler maybe the pieces would have fallen into place faster. Live and learn!