Learning Elm
Elm is "a delightful language for reliable webapps."— or so its homepage says. Today, I'm going to dive in head-first to find out what all the fuss is about.
For those of you unacquainted, here are some of Elm's purported features, straight from its homepage:
- Compiles to JavaScript: The language targets JavaScript specifically
- No runtime exceptions: Instead of producing runtime exceptions, Elm uses type inference to detect problems during compilation.
- Great performance: Like React, Elm uses a virtual DOM designed for simplicity and speed.
Sounds cool. Let's fire it up then, shall we?
Installing Elm
If you're on a Mac like I am, Elm has a Mac installer available here. The current stable version is 0.18. The '0' at the beginning doesn't give me much confidence in using Elm in production, but if Pivotal is doing it then maybe it's worth a try?
After completing, the installer tells us:
A bunch of useful programs were just placed in /usr/local/bin/
Installing the Vim plugin
Since I use Vim, I'm going to go ahead and install the elm.vim plugin:
$ cd ~/.vim/bundle
$ git clone https://github.com/lambdatoast/elm.vim.git
Easy enough.
Hello World
When learning a new language, there are two benchmarks of expertise: Hello World and Todo.
Elm appears to be no exception. Let's follow this Hello World tutorial on the elm-tutorial.org site.
Create a project folder
First we create a new folder for our Hello World application. I called mine
hello-elm
:
$ mkdir hello-elm
Install packages
It looks like first we need to install the package elm-lang/html
. Why that's
not installed by default I'm hopeful will be revealed later on...
$ elm package install elm-lang/html
Some new packages are needed. Here is the upgrade plan.
Install:
elm-lang/core 5.1.1
elm-lang/html 2.0.0
elm-lang/virtual-dom 2.0.4
Do you approve of this plan? [Y/n]
Ooh, do I approve? I feel like a boss. Yeah, I approve.
Starting downloads...
● elm-lang/virtual-dom 2.0.4
● elm-lang/html 2.0.0
● elm-lang/core 5.1.1
Packages configured successfully!
First impressions of package management: Elm is pretty freaking mature for a 0.18 release! Would be nice to see NPM/Yarn-style progress bars, but so far this feels solid.
The elm package install
command seems to have created the following stuff in
the directory:
elm-package.json
elm-stuff
Write some code
Okay, now we're going to create our first Elm module. Exciting. Here's mine:
module Hello exposing (..)
import Html exposing (text)
main =
text "Hello"
I put that in Hello.elm
. Right now it's not clear to me whether that should be
in the elm-stuff
directory or in the root directory. I put it in the root.
Now we run elm reactor
:
$ elm reactor
Interesting. So it looks like Elm has a sort of "dashboard" view of your files,
packages, and dependencies. Clicking on our Hello.elm
file takes us to our
application, which at this point is just the text "Hello" in the top-left corner
of the page.
Some thoughts about what I've seen so far:
- There's a module named Html. The fact that Elm treats the DOM as a first-class concept as opposed to relying on class-like abstractions as in standard JavaScript is a welcome change as we move toward functional UI paradigms.
- The compile time for a Hello World application seemed awfully long. I'm not sure if it was just a fluke on my machine, but it took 2-3 seconds before my app loaded. I could understand if we had a complex application, but this is Hello World...
Okay, let's keep going...
Rendering complex markup
So far, it looks like we can render text using the text
function. I'm digging
around in the documentation and found an examples
page. I poked around a few of these until I found
an example of rendering an unordered
list.
It looks like Elm's composition syntax is Lisp-ish, but not Lisp:
import Html exposing (li, text, ul)
import Html.Attributes exposing (class)
main =
ul [class "carbonated-beverages"]
[ li [] [text "La Croix"]
, li [] [text "Zevia"]
, li [] [text "Pepsi"]
]
Some observations here:
-
Because Elm is functional, even attributes need be imported. Whereas in
ES6, we need only import a module in order to gain access to all its getters
and setters, Elm requires explicitly importing things like
class
in order to use them. - It is a welcome change to see a main function that returns a DOM tree. In React, there's a bunch of boilerplate to attach a component to the DOM. In Elm, all of that boilerplate is baked into the language.
- The slow compile time appears to have been a fluke. This new example compiled almost instantly.
A word about Elm's architecture
It looks like Elm has its own application architecture that it lovingly calls the ... Elm Architecture.
It consists of:
- Model: the state of the application
- Update: a way to update the state of the application
- View: the resulting view of your state as HTML (or XML, or SVG)
From what I can tell, these roughly correspond to React/Redux concepts thusly:
- Elm's Model layer is equivalent to Redux's store
- Elm's Update layer is equivalent to Redux's actions
- Elm's View layer is equivalent to React itself
These analogies are made even more convincing when you consider the fact every value in Elm is immutable, just like Redux when used with Immutable.js, and that the Elm Architecture makes use of the one-way data flow paradigm made famous by Facebook's Flux.
Responding to user input
Okay, so we've produced a Hello World example and then rendered some slightly more complex markup. That's fine, but how does Elm deal with user input? Let's try creating an example where clicking a button renders the text "Hello Elm" below the button.
According to Lucas Reis's
blog, Elm supplies a
function Html.App.program
which automatically routes an application for the
Elm Architecture:
Elm apps use a centralized state pattern, which I've written about in this blog. It's a simple "loop" described as such:
Model > View > Update > Model > View > ...
First you describe a model of your app. It is the skeleton, the data you need to render the application.
The view is then a function of the model. It takes the data and renders it.
After rendering, the application "waits" for user interaction or any other event. When that happens, it triggers the update function. The update function is a function that receives the old model and data of the event, and returns a new model. This model is then rendered, and the loop continues.
Evidently, there's a simpler version of this function supplied with Elm called
beginnerProgram
. I couldn't find a decent explanation anywhere for what this
function does that the regular program
doesn't (or vice versa). If you know,
please leave a comment!
Here's the full text of the user input example I just created:
import Html exposing (Html, button, div, text, p)
import Html.Events exposing (onClick)
type alias Model =
String
model : Model
model = ""
type Msg = ShowGreeting
update : Msg -> Model -> Model
update msg model =
case msg of
ShowGreeting ->
"Hello Elm"
view : Model -> Html Msg
view model =
div [ ]
[ button
[ onClick ShowGreeting ]
[ text "Click Me" ]
, p [ ] [ text model ]
]
main =
Html.beginnerProgram
{ model = model
, view = view
, update = update
}
Let's walk through this line by line together.
First, we import some HTML functions we'll use to render our markup and listen to the button for clicks:
import Html exposing (Html, button, div, text, p)
import Html.Events exposing (onClick)
Then we'll create a new type called Model
which is an alias for a string:
type alias Model =
String
Next, we'll create an instance of the Model
type called model
and initialize
it to an empty string literal:
model : Model
model = ""
Then, we'll set up our update
function. This confused me at first, so maybe my
explanation will help you if you come from the Redux universe.
The update
function in Elm works essentially like reducers in Redux. It takes
a message (an action in Redux-speak) and the model (the state in Redux-speak)
and returns a newly mutated model.
Except Elm is much more suited to this paradigm for the following reasons:
- Elm's union types make the usage of symbolic enumerations like user actions checkable at compile time instead of relying on runtime checks.
- Because Elm data is always immutable, state reduction doesn't require clunky external libraries like Immutable.js.
So our update
function takes a msg
(in this case, only the value
ShowGreeting
), and returns a new model state based on whatever msg
it
received:
type Msg = ShowGreeting
update : Msg -> Model -> Model
update msg model =
case msg of
ShowGreeting ->
"Hello Elm"
Okay, now that we've defined how our model will change in response to user actions, let's define the view. Again, Elm comes to the rescue by allowing us to pass the model directly into the view function. No complex binding a la Redux containers!
view : Model -> Html Msg
view model =
div [ ]
[ button
[ onClick ShowGreeting ]
[ text "Click Me" ]
, p [ ] [ text model ]
]
Notice how we're passing model
to the text
function inside the p
tag. When
we boot the application, the model's value will be an empty string, but as soon
as we click the button, we fire the ShowGreeting
message and the model changes
to the string "Hello Elm"
.
Finally, we glue everything together via the beginnerProgram
function:
main =
Html.beginnerProgram
{ model = model
, view = view
, update = update
}
My impressions
All in all, I was pretty impressed with Elm after playing with it for an hour or so. While it's clear the framework has a ways to go in terms of documentation and external library support, its philosophy removes a lot of the boilerplate that plagues modern JavaScript tooling. In addition, it takes concepts like immutability, functional programming, one-way data flow, and type safety which are must-haves in today's JavaScript development world, and makes them first-class members of the language.
Would I use Elm in a production project yet? I'm not sure if I'd subject my clients' businesses to that risk just yet. While it's impressive, the idea of relying on a small community for support on complex topics like JavaScript interoperability would make me a bit cautious. But I'll definitely consider Elm the next time I build a personal project.