Elm in the Real World
I’ve had my eye on Elm for some time now. I remember reading about it when it was version < 0.10. The concepts it proposed were fantastic and really resonated with me at the time because I had also just got myself into Erlang for a few months prior. You could probably say that because I liked Erlang I had an easier time liking Elm, even when it was very young, because I had become a fan of functional languages and immutable data.
However, Elm can be very demanding. And the biggest downside in my mind is the awful compiler messages. While the compiler is very intelligent and well-written, the error messages are usually of the “vague at best” variety for new Elm developers. And the concepts — though I do like them — can be very difficult to implement realistically even if you think you understand it theoretically.
So here’s my “real world” experience with developing for Elm. And although I’ve been keeping an eye on it for awhile, this is the first time I’ve actually ever dug into it and actually done any programming with it. So I’m coming at Elm development from almost square one.
This should not surprise anyone, but starting simple is best. I personally decided to get my feet wet in a small side project I have involving a character editor for a Mutants & Masterminds game I’m going to be running soon. But I don’t tend to go dead-simple when I do these things. I like to add a curveball to really get to know the language I’m to be learning.
The game involves keeping track of a plethora of stats, as is usually the case with these types of games, so I decided to implement a web page that listed all possible character Attributes and allowed you to modify them. No validation or anything. Simply output a list of Attributes, names and descriptions, with an input field to modify the rank (i.e. value). But here’s the kicker: the Attribute list was to be pulled from an API.
I have to give thanks to this wonderful post on using Elm to interact with an API. Without it I’m sure this whole learning process would have taken significantly longer than it did.
Elm and Ports
All of that is relatively straight-forward though, and here was my problem: I knew that the ‘main’ function of a module would be executed when it was embedded into a web page, but the port function described in the blog post I read was also being executed immediately. My first thought was that maybe all ports run once at the beginning of code execution, which seems ridiculous, but maybe that’s just how things work. So I renamed the port function and added a section, similar, port function that did something slightly differently. Neither executed. Hmm.
After some research, I found an implicit rule: if a port function named “runner” exists, it will be executed immediately after code loading. Neither the blog post I was reading nor any official Elm documentation explicitly stated this rule, so I’m stating it here and now. If you have a task you wish to run immediately after your module is embedded into a web page, create a “port runner” function. And if you have multiple tasks you want to run, use the Task module for functions like “map”, “andThen”, and “sequence” to trigger more than one task.
The first thing to know: decoding into a Record is probably the easiest way to interact with JSON data. Create a type and use the Json.Decode.object* functions to parse a well-defined JSON blob into a well-defined Elm structure.
Second thing to know: order is important. If you define properties “a”, “b”, and “c” of a record — in that order — then you must decode your JSON in that order when using Json.Decode.object*. Note: this does not mean that the actual JSON blob needs to have any particular order to its own data, only that the data exists. Even when using a function like (“id” := Decode.int) you aren’t saying “parse ‘id’ and put it in the ‘id’ property”, you’re simply saying “give me the value of ‘id’ as an integer” and it will shove it into the record based on the order it’s executed in your function call.
Third thing to know: use “oneOf” and “succeed” for default values. For example, I had a property that was purely used on the client and not being stored on the server, so it wasn’t being sent down via the API right now but it could be in the future. To create a “parse value or give me a default” you use a function such as (oneOf [“property” := type, succeed default]). That’s the general form, and it ensures that you get a value even if the property you want to parse doesn’t exist in the JSON blob. Just make sure the types of ‘type’ and ‘default’ match or you could get errors.
Modifying Data in a List
One of the other problems I ran into seemed like a simple one. You have one piece of data in a list that you want to edit; how do you do that in Elm? Thankfully I had made a lot of progress with functional programming via Erlang, so I had a good idea of solving this one, but I figured I would still put it here for honorable mentions.
Because Elm uses immutable data, you can’t simply say “keep the same list and just replace one thing in it.” You also can’t modify the item in the list and just return the list back. If you wanted to remove an element you would simply iterate over the list, applying a filter to each item, and only adding successful items to a new list; the result being a list without the item(s) you didn’t want. So to modify one or more elements in the list we need to take the same approach: build a new list.
We can very simply use List.map to do the brunt of the work, only supplying a function that operates on each element in the list. And if we’re looking to only modify a subset of elements, we apply a filter to each element. If the element passes, modify it. If it does not pass, return it without changes. It would look something like this:
Using function currying we can build much more inventive ways of modifying data in a list that work with other core constructs and concepts in the Elm language.
At first that looks like a lot more work going on, but really it opens up a framework to easily creating highly modular code that can work with almost any data you plug into it. And if you’re wondering about function currying…
One of the things I love about Elm is the function currying. You can read the full rundown at the link supplied, but the short version is this: if you have a function, you can create a new function that has “applied” a subset of the parameters and accepts the remaining parameters. This might be difficult to conceptualize, so let’s go with an example.
Imagine you have a function with the following type definition:
We have a function that checks if some string is contained in a list of strings. Standard, simple, no problem. But what if we had a list of strings we wanted to run through this function? Or what if the target string was coming in through user input and might not pass validation? We can use our parentheses to curry the function in a way that makes it reusable in different situations.
Using it to check a list of strings:
Using it to check input wrapped in a result:
We passed in a subset of arguments to the “contains” function (in this case the list of strings to check against) to create a curried function that accepts the remaining arguments (in this case a string to check for). The curried function would have a type definition of:
You’re probably already thinking of ways that is useful, but let me relate the moment it really clicked for me.
I was accepting two possible user inputs that were to be integers. In the case that either input was not an integer then I would ignore it and revert the to the last-known good state. If they were both valid integers then I would perform an operation on them that included a list of data I had. This is what I did:
That’s very succinct and easy to read. The Result.map2 function takes in a function accepting two arguments and two Results; if either Result is Err, it returns the error, otherwise it applies the values to the function and returns Ok result. But this is only possible because of the particular function we created. If we did not have function currying, and instead had to apply all required parameters immediately, we would need to follow a very different code path:
While not overwhelmingly difficult to read, it definitely is not as simple to deduce (or maintain!) as the previous form using function currying.
Overall I really enjoy Elm and will continue to use it in my personal projects where it makes sense. Right now I’m building my own blogging engine, just so I can play around with some new tech and learn a few things while also giving myself a custom blog for personal use. I plan to use Elm for the administrative interface.
The public viewing of posts and such will all be static files, generated on-demand by the backend as is necessary. After that’s done, I’ll build out an API to return JSON dumps of user and post data. Admin users will be capable of logging into an Elm-powered web page that will be used to create, edit, and delete posts and manage users. Just the basics to start with.