Adventures in Elm
We’ve been hearing a lot about Elm lately, especially how it claims to prevent errors. Our newest contributor, Matt Granmoe, will explore it, how to get started, and some thoughts on error prevention and debugging. Matt will be exploring error handling and debugging in various JavaScript and web technologies with us. Matt is a JavaScript developer, progressive bassist, blogger, Filipino martial arts practitioner and active contributor to various libraries within the React/Redux ecosystem.
Elm is Made of Haskell
Elm’s homepage touts itself as “a delightful language for building reliable webapps,” promising great performance without runtime exceptions. The nearly pure functional language takes most of its syntax and concepts from Haskell and ML, and it aims to marry the best of functional programming with HTML, CSS and JavaScript interoperability and a strong focus on being user friendly and producing clean, concise, expressive code with minimal tooling. Think of it like the Python of pure functional languages. Elm is statically typed via a Hindley-Milner type system and compiles to JavaScript.
Elm takes a rather blatant preference for functional programming:
Why a functional language?
Forget what you have heard about functional programming. Fancy words, weird ideas, bad tooling. Barf. Elm is about:
- No runtime errors in practice. No null. No undefined is not a function.
- Friendly error messages that help you add features more quickly.
- Well-architected code that stays well-architected as your app grows.
- Automatically enforced semantic versioning for all Elm packages.
Redux, I am Your Father
The Elm architecture inspired the Redux JavaScript library. This pattern has been gaining notice recently as “JavaScript fatigue” reaches an all time high, with more JavaScript developers willing to give non-JavaScript solutions a chance. Since Elm inspired Redux, mandates immutability and declarative programming, and has its own virtual DOM implementation, maybe it isn’t a far cry for those of us who have used the React ecosystem.
Let’s explore what it’s like to work with Elm. How do we handle errors? How do we debug? Is it as beginner-friendly as it claims?
Setting Up
First, I need to get everything installed. After running the Mac installer, I have all the Elm goodies on my PATH
: elm-repl
, elm-reactor
, elm-make
, and elm-package
. That took all of 3 minutes. No hiccups so far. Following the Elm instructions, I next installed the “elm” extension for VS Code. After installing, I can create an Elm file and get nice syntax highlighting, as shown in their example gif:
I also installed the elm-format
extension. So far so good! The remainder of the docs cover the Elm language and Elm architecture. I read through these fairly brief overviews, but I won’t rehash them here. Let’s try to get a simple app running.
Hello, Elm
On the command line:
Per the docs, elm-reactor is a way to “get a project going quickly.” We should be able to create a Main.elm file, put some code in it, and run elm-reactor in order to build and run the file in the browser. Let’s try that. I opened the “hello-elm” directory we just created in VS Code, and I see an “elm-stuff” directory and an “elm-package.json” file. Now I’ll create a file called “Main.elm” (following Elm file name conventions, the entry module is called “Main” and all modules are capitalized). I’ve added these lines in the file as an initial test:
Now to run it via elm-reactor. On the command line in our project directory:
Now we just go to localhost:8000 and click the “Main.elm” link to build and run our module.
Success! So far there’s been absolutely no fuss following the setup and first app instructions.
Using the Elm architecture
Now let’s try the canonical counter example given in the docs. Replace the contents of Main.elm with the following:
It works!
We now have a working example of the Elm architecture.
Compilation
So far I’ve been impressed with the out of the box features and simplicity of Elm’s tooling, but how does Elm handle compiler errors? Can we sneak anything past the compiler? Let’s give that a shot. I’ll make an intentional mistake in Main.elm and try to use something that is undefined:
First off, notice that the VS Code Elm extension catches this. Awesome! Now let’s see what elm-reactor has to say about it:
Not only is Elm refusing to compile this, it gives us some suggestions for a possible fix. The error messaging overall is pretty useful. This is a much needed departure from the cryptically academic error messages of the older functional languages which inspired Elm.
I tried this:
and VS Code Elm extension told me “model does not have a field named ‘fake’.” After working in only JavaScript for the past two years, this seems pretty cool! Lo and behold, the same output is also shown in elm-reactor when we try to run the code:
Well, the compiler foiled us at every turn, but one way to crash Elm at runtime is to do so on purpose with the tool they created for this job!
This renders the app unresponsive and gives us an uncaught JavaScript runtime exception:
Debugging Elm
The Elm compiler attempts to remove logical errors in your code, but the app is still going to run on the web (“the most hostile software engineering environment imaginable”). Errors are going to happen from timing failures, incompatible browsers, and invasive plugins. We’re not going to get very far without monitoring and debugging. So how does Elm’s debugger work? Can I trace variables? Can I set breakpoints and inspect? Much to my surprise and disappointment, the official guide has no information about debugging!
However, I did notice when running Elm files via reactor that there’s a built in debugger attached.
This feels very similar to Redux devtools (reverse and replay actions, see the app state after any given action, etc), but, despite its benefits, this tool doesn’t meet my expectations for overall debuggability.
I found very few trustworthy and up to date resources on Elm debugging. I tried piecing together what scant information I could dig up via google, but all the sources seemed to conflict. For example, most of the debugger functions don’t seem to be present in the version of Elm I have installed (0.18.0, the latest official release at the time of writing). Luckily, VS Code saved me.
So it appears that Debug
only has two methods, crash
and log
, one of which we’ve already used. Let’s try the other one. Gleaning what I can from the method signature, I tried using log
like this:
I’m inferring that Debug.log
takes one string argument to use in the log output, one argument of any type of value, and then returns the second argument so that the debug expression evaluates to that argument (is replaced by it) making it a no-op in relation to the surrounding code. Let’s see if elm-reactor agrees with me:
It works! Cool.
I’ve been able to fairly easily figure out how to use the native Debug
module in Elm thanks VS Code’s Elm integration. However, the lack of documentation and apparent instability of the approach to debugging in Elm is the only pain point I’ve come across so far.
Monitoring Elm
Under the covers, the debug module will expose messages through the console
object, where TrackJS will automatically capture them as part of the Telemetry Timeline. Integrating TrackJS monitoring into your Elm app is pretty straightforward. First we need to make an index.html with elm-make
:
Taking a look at the output file, we can see some clean HTML and JavaScript. Simply paste the TrackJS installation snippet before the <script>
generated by Elm
Tradeoffs of Compilation
Since we’re leaving everything to the compiler, we get no sourcemaps for production debugging. Sourcemaps have been brought up as a feature request for Elm, but they have all been denied per the argument that you only need sourcemaps if you expect runtime exceptions in the compiler output. If an error happens, it’s considered a bug in the compiler and treated as a top priority. This also means the Elm development work cycle is different than the JavaScript one. Instead of the process being: write code, debug in the browser, modify code, repeat; our process is: write code, compile, modify code per any compiler errors, repeat.
Compiler errors are surprisingly useful in Elm, however, if for any reason we did need to debug the user experience of our app, we would be stranded without sourcemaps. In this sense, using Elm instead of JavaScript means trading runtime debuggability for the compiler’s guarantee of no runtime exceptions.
This is the web, so debugging continues to be absolutely necessary. Luckily, debugging Elm’s compiled JavaScript at runtime is fairly straightforward.
Debugging is also a desirable tool for development in Elm when our code compiles, but does not behave as expected. For example, bottomless recursion will not cause a compiler error:
This will cause the JavaScript thread to lock up, making the app unresponsive.
Wrapping Up
In this post, we became acquainted with the reasoning behind Elm, used its built-in tools and development flow, tried out the Elm architecture, built a trivial frontend web application in Elm, and learned to do some basic monitoring and debugging. If you’re building in Elm, or any other JavaScript tool, be sure to check out TrackJS so you know when it breaks! Grab a free 14 day trial today.