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
:
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
:
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:
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.