When we introduced Elm to our production front-end at NoRedInk, we knew what to expect: the productivity benefits of an incredibly well-built language, and the intrinsic risk of integrating a cutting-edge technology for the first time. Knowing these risks, we came up with a minimally invasive way to introduce Elm to our stack, which let us get a sense of its benefits without having to make a serious commitment.
Since several people have asked me about this, I’d like to share how we did it!
This walkthrough assumes only a beginner level of Elm familiarity. I won’t be spending much time on syntax (though I will link to the syntax guide as a reference) or functional programming staples like map and filter, so I can go into more depth on introducing Elm to an existing JS codebase.
Let’s dive in!
Elm for…business logic?
Elm is known for its UI-crafting capabilities (both as an improvement over React as well as for game programming), but building out an entire UI in a new language was more commitment than we wanted to tackle as a starting point. We didn’t want a drink from the fire hose; we just wanted a little sip.
Fortunately, it turns out Elm is also a phenomenal language for writing business logic.
Besides making it easy to write clear, expressive code, Elm also makes it easy to write code that’s rock-solid reliable. How reliable? Here are some numbers:
- Students answer over 2.5 million questions per day on NoRedInk. Needless to say, edge cases happen.
- 3% of our code base (so far!) is Elm code. Not a ton, but a substantial chunk.
- We have yet to see our production Elm code cause a single runtime exception.
This is not because we sheltered it; our production Elm code implements some of the most complicated parts of our most mission-critical features. Despite that level of exposure, every runtime browser exception we’ve ever encountered—in the history of the company—has been fixed by changing CoffeeScript or Ruby code. Never by changing Elm code.
This is also not because we have some particularly unusual code base; rather, it’s because Elm’s compiler is astonishingly helpful.
Let’s get a taste for that by introducing Elm to an existing JavaScript code base!
Adding a hint of Elm to a JS TodoMVC app
We’ve been using React and Flux at NoRedInk since they came out in 2014.
Flux stores are a convenient place to introduce Elm to your code base, because they’re all about business logic and are not concerned with UI rendering. Porting a single Flux store to Elm proved a great starting point for us; it was enough to start seeing real benefits, but not such a big commitment that we couldn’t have changed our minds and turned back if we’d wanted to.
Let’s walk through doing the same thing: porting a store from the Flux TodoMVC example to Elm.
First, grab Facebook’s Flux repo. (I’m linking to a fork of their repo, in case theirs has changed since I wrote this.) and cd
into its directory. At this point you should be able to follow the instructions for building the TodoMVC example.
git clone git@github.com:rtfeldman/flux.git cd examples/flux-todomvc npm install npm start
Once you have npm start
running, you should be able to open the index.html
file in your current directory and see a working app.
If you’re not familiar with the Flux architecture (it’s conceptually similar to The Elm Architecture), now might be a good time to read about it. You don’t need a ton of experience with it to follow along, but having a general idea what’s going on should help.
Now you’ll want to grab yourself a copy of Elm. Either use npm install -g elm
or visit elm-lang.org/install to get an installer. When you’re finished, run elm make --help
in your terminal and make sure it says Elm Platform 0.15.1
at the top!
Integrating Elm at a basic level requires three things:
- A
.elm
file containing our Elm code. - An elm-package.json file specifying our dependencies.
- A JavaScript command to invoke our Elm code.
Let’s create examples/flux-todomvc/TodoStore.elm
with the following code:
module TodoStore where
port dispatchCreate : Signal String
Now let’s build it by running this command in the examples/flux-todomvc/
directory:
$ elm make TodoStore.elm --output elm-todomvc.js
The first time you build, you should see this prompt:
Some new packages are needed. Here is the upgrade plan. Install: elm-lang/core 2.1.0 Do you approve of this plan? (y/n)
After you answer y
, Elm will install your dependencies. When it’s done, you should see this message:
Successfully generated elm-todomvc.js.
So far, so good!
Your directory should now contain an elm-package.json file and the compiled elm-todomvc.js
file generated by elm make
, as well as an an elm-stuff/
folder which holds downloaded packages and the like. I always add elm-stuff/
to .gitignore
.
Next we need to add our compiled Elm code to the page.
If we’d incorporated gulp-elm into our project’s Gulpfile (see also grunt-elm, elm-webpack-loader, and this Gist for Sprockets integration), then our compiled Elm code would already be included as part of bundle.js. However, we’re avoiding build tool plugins for this example, so we need to tell our index.html file about elm-todomvc.js by adding a new <script>
tag above the one that imports "js/bundle.js"
like so:
...
<script src="elm-todomvc.js"></script>
<script src="js/bundle.js"></script>
</body>
...
All done! Now our Elm code is compiled and available on the page; we’re ready to start porting.
Porting Over The Wiring
(Note: If you’re more interested in business logic than wiring, feel free to skip this part and grab the completed wiring from this Gist so you can move on to the next section. If you’re curious about the details of where this code came from, you can always return here later!)
We’ve written some Elm code, compiled it, and added the compiled code to our page. Now let’s start porting TodoStore.js to Elm.
I’ve found it’s easiest to start by porting over the wiring. That gives us a foundation for a basic runnable app, which makes the rest of the process go more smoothly.
We’ll start with TodoConstants.js, which represents the different types of Actions our program can use. We don’t need a whole separate file for this, nor do we need the keyMirror
library; Elm’s built-in union types are perfect for representing Actions, as they let us incorporate payload information along with the Action’s name.
That means we go from this code in TodoConstants.js…
module.exports = keyMirror({
TODO_CREATE: null,
TODO_COMPLETE: null,
TODO_DESTROY: null,
TODO_DESTROY_COMPLETED: null,
TODO_TOGGLE_COMPLETE_ALL: null,
TODO_UNDO_COMPLETE: null,
TODO_UPDATE_TEXT: null
});
…to this code in TodoStore.elm, right after the module TodoStore where
part:
type Action
= Create String
| Complete TodoId
| Destroy TodoId
| DestroyCompleted
| ToggleCompleteAll
| UndoComplete TodoId
| UpdateText TodoId String
Note that each of these specifies not only the name of the Action
type, but also what kind of payload the action requires. Create requires a String
for the text of the item to be created. Complete
takes the ID of the TodoItem
to mark as completed. We can have as many or as few payload parameters as we like; DestroyCompleted
has no payload, and UpdateText
takes two values.
Whereas String
is a built-in type, TodoId
is a custom one I just made up. Let’s define it right below our type Action
declaration:
type alias TodoId = Int
This just says “TodoId
is equivalent to Int
, so anywhere you see a TodoId
, treat it as an Int
.” This isn’t necessary, but it lets us write more self-documenting code; it’s certainly easier to tell what kind of payload Destroy TodoId
accepts than if we’d written Destroy Int
instead! This is also handy because if it means later we want TodoId
to be a String
, we can change this one line instead of having to change it in several places in our Action
declaration.
Next we need to define our API for Elm-to-JS communications. We’ll want to define an inbound port for each of the store’s methods, like so:
port dispatchCreate : Signal String
port dispatchComplete : Signal TodoId
port dispatchDestroy : Signal TodoId
port dispatchDestroyCompleted : Signal ()
port dispatchToggleCompleteAll : Signal ()
port dispatchUndoComplete : Signal TodoId
port dispatchUpdateText : Signal (TodoId, String)
Note that these line up very closely with the Action
definition we just gave. That’s not a coincidence! We did that because we want to translate these raw inbound JavaScript values into Action
instances, so we can work with them more easily later. Let’s do that:
actions =
Signal.mergeMany
[
Signal.map Create dispatchCreate,
Signal.map Complete dispatchComplete,
Signal.map Destroy dispatchDestroy,
Signal.map (always DestroyCompleted) dispatchDestroyCompleted,
Signal.map (always ToggleCompleteAll) dispatchToggleCompleteAll,
Signal.map UndoComplete dispatchUndoComplete,
Signal.map (\(id, text) -> UpdateText id text) dispatchUpdateText
]
Here we’re using Signal.mergeMany and Signal.map to go from seven inbound port Signals, each containing raw JavaScript values, to one Signal of Actions, which will be much easier to work with!
Most of these conversions look the same; for example, Signal.map Create dispatchCreate
takes dispatchCreate
(an inbound port which is represented as a Signal String
) and passes its String
payload to Create
, which gives us back an Action
with the appropriate name and payload.
Since DestroyCompleted
doesn’t have a payload and therefore doesn’t take an argument, we use always to throw away the argument Signal.map
will try to pass it from dispatchDestroyCompleted
. Similarly, since UpdateText
takes two arguments but dispatchUpdateText produces a single tuple, we use an anonymous function to destructure that tuple and pass its components as separate arguments to UpdateText
.
We’re almost there! Now all we need is an initial model and a way to send updates to that model out to TodoStore.js. To do that, let’s add this code right above our current port declarations:
update action model =
model
initialModel =
{
todos = [],
uid = 1
}
modelChanges =
Signal.foldp update initialModel actions
port todoListChanges : Signal (List TodoItem)
port todoListChanges = Signal.dropRepeats (Signal.map .todos modelChanges)
This connects everything together.
Now we have an initial model with an empty list of TodoItem
instances (we’ll come back to uid in the next section), we have a modelChanges Signal
that represents changes to that model over time, and we have an update function which translates one model to another using an Action
it received from all those JS ports we merged together and mapped over.
Finally, we add a single outbound port, todosChanged
, which sends a revised todos list out to TodoStore.js every time that part of the model changes.
Onward to the business logic!
Porting the “Create Item” Feature
Now that we have a basic TodoStore.elm with a bunch of wiring ported over, we can start porting logic from TodoStore.js over to TodoStore.elm.
Our goals will be:
- Replace all the code in each branch of the AppDispatcher.register handler’s switch statement with one-liner calls to invoke the appropriate Elm code.
- Confine our changes entirely to TodoStore.js, which means: a. Don’t alter any other files besides TodoStore.js (except for adding the new TodoStore.elm) b. Don’t change the public-facing API of TodoStore.js, including event-emitting logic. c. End up with the rest of the app continuing to work exactly as it used to
This was what we did when we first introduced Elm at NoRedInk. Having the peace of mind that only one JS file was changing, and that its public-facing API would remain exactly the same, meant that introducing Elm was a very low-risk undertaking.
To do this, let’s start by invoking our Elm code. Add the following to TodoStore.js, right above the call to AppDispatcher.register
:
var defaultValues = {
dispatchCreate: "",
dispatchComplete: 0,
dispatchDestroy: 0,
dispatchDestroyCompleted: [],
dispatchToggleCompleteAll: [],
dispatchUndoComplete: 0,
dispatchUpdateText: [0, ""]
};
var ports = Elm.worker(Elm.TodoStore, defaultValues).ports;
ports.todoListChanges.subscribe(function(updatedTodoList) {
// Convert from the flat list we're using in Elm
// to the keyed-by-id object the JS code expects.
_todos = {};
updatedTodoList.forEach(function(item) {
_todos[item.id] = item;
});
TodoStore.emitChange();
});
ports.dispatchCreate.send("Gotta port more JS to Elm!");
// Register callback to handle all updates
AppDispatcher.register(function(action) {
If you bring up the app, the page should render with no JavaScript errors. We haven’t wired up any interaction yet, but if the page renders with no errors, we know we’ve connected the ports successfully. If we had (for example) forgotten to specify a default value for one of our ports, we would see an empty Todo list and an error message in the console.
Now let’s finish up Create
.
Besides making our code more self-documenting, type alias can also save us from writing boilerplate; the type alias we defined earlier for TodoItem gets us a “constructor function” for free, which is roughly equivalent to the following JavaScript function:
function TodoItem(id, text, complete) {
return {
id: id,
text: text,
complete: complete
};
}
So now instead of writing this:
{ id = 42, text = "pick up groceries", complete = False }
…we can write this:
TodoItem 42 "pick up groceries" False
Notice that this automatically-generated function has a lot in common with the handwritten create function in TodoStore.js; so much, in fact, that porting over create wouldn’t really get us much. (It’s not like we needed to keep that funky way of simulating a server-generated id!)
We do, however, need to port over the dispatcher logic. We’ll do that inside our update function from The Elm Architecture, which we will now expand from just returning model to doing the following:
update action model =
case action of
Create untrimmedText ->
let
text =
String.trim untrimmedText
in
if String.isEmpty text then
model
else
{ model |
todos <- model.todos ++ [TodoItem model.uid text False],
uid <- model.uid + 1
}
_ ->
model
Since this code uses String.trim, we’ll need to import String below the first line of the file, like so:
module TodoStore where
import String
There are a few syntactic differences between the Elm version of this logic and the JS version - let instead of var, case instead of switch, and record update syntax for the whole { model | todos <- … }
part - but the Elm code is accomplishing the same thing as the JS code did.
Note that because we’re using the Action union type we defined earlier, case can not only match on Create, but also extract its payload (recall that we defined Create to have a String payload), and name that payload untrimmedText. Handy!
The main logical change from the Flux version is that (like all Elm functions) update is a stateless function; it neither mutates any shared state nor has any side effects. All it does is accept a model as an argument and return a new one. So rather than mutating the _todos collection when we create a new item, we instead return a new model that has the new item appended. (Note that Elm uses persistent data structures to store its immutable data, which makes operations like this fast and efficient.)
As for uid
, it’s our replacement for the original simulate-an-id code. In a production application we’d get our new id values from a server, but since we don’t have a server here, we need to generate them ourselves. It doesn’t matter what our id values are, so long as they’re unique, so we just store a counter called uid
in our model and increment it every time we use its current value to represent a unique id.
Finally, the _ -> model at the end provides a default handler for our case expression. Currently Create untrimmedText -> is the only other case we’ve defined, and this essentially says “if we didn’t match any of the previous cases, then just return the original model.”
Now that we’ve specified what a Create
action does, we need to replace the current JS implementation with our Elm one. To make that happen, we replace the handler code for TODO_CREATE
with a call to send the relevant data to an Elm port instead:
case TodoConstants.TODO_CREATE:
ports.dispatchCreate.send(action.text);
break;
We have now completely delegated the logic for creating todo items to Elm. Try it out! You will be able to add todo items to the list.
Just for fun, let’s verify that we’re using TodoStore.elm instead of TodoStore.js for our logic. Comment out this line in TodoStore.elm, and replace it with the one below:
-- todos <- model.todos ++ [TodoItem model.uid text False],
todos <- model.todos ++ [TodoItem model.uid "Hodor" False],
Run elm-make TodoStore.elm --output elm-todomvc.js
again to compile the Elm code, and reload index.html
. Now whenever you try to enter a todo item, it will disregard your input and add “Hodor” instead.
Note that at this point, the rest of the app continues to function as normal. We’ve replaced one piece of logic with Elm code, but we haven’t broken anything to do so. This is by design! We set out to introduce Elm incrementally to this code base, and that’s exactly what we’re doing.
Let’s revert the “Hodor” line of code to the way it used to be, and while we’re at it, comment out this line that we just used to confirm things were wired up properly:
// ports.dispatchCreate.send("Gotta port more JS to Elm!");
With this foundation in place, we’re ready to port the rest of the store logic to Elm!
Porting the Remaining Features
With this foundation in place, we’re ready to port the rest of the store logic to Elm!
Let’s do Destroy
next. Here’s what we need to add to our case expression, right above the _ ->
model clause at the end:
Destroy id ->
let
todosWithoutId =
List.filter (\item -> item.id /= id) model.todos
in
{ model | todos <- todosWithoutId }
And then we replace the implementation in TodoStore.js with a call to a port:
case TodoConstants.TODO_DESTROY:
ports.dispatchDestroy.send(action.id);
break;
Pretty simple, right? If you compile your Elm code, the delete button should work as before.
DestroyCompleted
can be a one-liner. Here it is on the Elm side:
DestroyCompleted ->
{ model | todos <- List.filter (\item -> not item.complete) model.todos }
…and then on the JS side:
case TodoConstants.TODO_DESTROY_COMPLETED:
ports.dispatchDestroyCompleted.send([]);
break;
Note that ports must always accept a value, but since this port has no meaningful information to send, we declare it as a Signal ()
on the Elm side and then always send []
on the JS side.
Since Complete
and UndoComplete
share common logic, we benefit from a helper function. Add this to TodoStore.elm, above the case expression:
withComplete complete id item =
if item.id == id then
{ item | complete <- complete }
else
item
This lets us add case clauses for Complete
and UndoComplete
as quick one-liners:
Complete id ->
{ model | todos <- List.map (withComplete True id) model.todos }
UndoComplete id ->
{ model | todos <- List.map (withComplete False id) model.todos }
See if you can wire these up in TodoStore.js yourself. (Spoiler: it’s the same procedure we used for Destroy
!) Afterwards, run elm-make TodoStore.elm --output elm-todomvc.js
again and verify that checking and unchecking todo items still works. You should also see a “Clear completed” button appear after you’ve checked one or more items, and clicking it should delete the checked ones.
At this point, all we have left to port are UpdateText
and ToggleCompleteAll
. Neither of them is doing anything we haven’t seen before, so I’ll just link to their implementations in the final TodoStore.elm, along with the requisite dispatcher code:
case TodoConstants.TODO_TOGGLE_COMPLETE_ALL:
ports.dispatchToggleCompleteAll.send([]);
break;
case TodoConstants.TODO_UPDATE_TEXT:
ports.dispatchUpdateText.send([action.id, action.text]);
break;
All done!
Takeaways
Here’s the final diff of what we did.
We kept our changes entirely localized to TodoStore.js, and we did not touch a single other .js file. Even better, the app as a whole continued functioning as we were porting over features, so this was a really low-risk change!
Compared to the original TodoStore.js, the final TodoStore.elm has several benefits:
- Our wiring got more useful:
- We replaced the original create function with the more self-documenting and featureful type alias TodoItem (and the “constructor function” that came with it for free).
- We represented TodoConstants as a union type we called Action, which captured additional information about our actions and made our case expression branches more concise and self-documenting.
- We removed the need for bespoke event emitting. Now we only call emitChange in one place, and Elm’s Signal.dropRepeats ensures we emit change events exactly when we ought to: every time the todo list actually changes, and at no other times.
- Everything is immutable, so we no longer have to track down accidental-mutation bugs.
- There are no side effects, so it’s extremely clear which functions can affect which others.
- The code is now rock-solid. The compiler will catch so many mistakes for us, it’s now extremely unlikely that we’ll accidentally let a runtime exception slip into production.
This is the same procedure we used to port our first Flux store to Elm at NoRedInk. That code has been running in production for months now, and has yet to cause a runtime exception!
Getting that first low-risk change into production gave us the confidence (and motivation!) to expand our use of Elm on future projects. The next major UI feature we shipped was Elm through and through, with elm-html and The Elm Architecture completely replacing React and Flux. It’s been great! That code is so easy to refactor, it gets more casual polish than any other part of our front-end code base; stay tuned for a future post about it.
By the way, if working on Elm code in production appeals to you…we’re hiring!
Discuss this post on Hacker News
Richard Feldman
@rtfeldman
Engineer at NoRedInk
Thanks to Aaron VonderHaar, Charles Comstock, Mike Clark, Jessica Kerr, and
Texas Toland for reading drafts of this.