If you’ve ever worked with Redux – in the context of a React application or not – you may have heard numerous times that it was inspired not only by Flux (which it followed) but also by the Elm architecture. This is something that is thrown around a lot by people in the React ecosystem, and looking at the Elm homepage it may seem difficult to see the link between a strictly-typed language and a JS state management library.
Quick menu
- What is Elm
- Setup
- First contact with Elm
- I want to get off Mr. REPL’s wild ride
- Setting up our web application
- Compiling our application
- The Elm architecture
- Setting up a build system
- Setting up a basic Webpack build
- Setting up hot reloading
- Advanced messaging and user input
- Elm: The Conclusion
- The Good Stuff
- The Maybe Not As Good Stuff
- The Subjective Median Opinion of the Stuff
- Would you like to know more?
What is Elm?
Elm is both a language and an architecture – it is a functional, compiled to JS, strictly-typed, immutable and pure, opinionated language to build web applications. Part of the language’s opinion is that your application should be structured a very precise way, which we call the Elm architecture. It is a very fascinating language with a pretty long list of benefits, to only cite a few:
- No runtime exceptions. Now that does not mean that there will never be any bugs in your application but the compiler is one of the best compilers I’ve ever seen and will not compile if it detects that your code is not sound. If you’re familiar with Javascript and having dozens of random errors in your console or that only happen on edge cases, this is the main benefit for me and one that way outweighs any cons of the language.
- Great performance: Elm is fast, very fast, and it is so by default, there’s no pattern to know, or optimizations techniques to learn, it is fast because it does an incredible amount of optimizations underneath (as is often the case with strictly-typed languages, because they have more informations on what you do).
- Elm puts emphasis on being explicit and avoiding the unknown. And this has ramifications throughout everything. A simple example: Semver is strictly enforced on all packages. What that means is that people don’t “really” version their packages: instead, the package manager analyzes what changed in the codebase and tags new versions itself. As a package consumer, you can then easily upgrade your dependencies as Elm will show you a summary of the API changes and what you need to change in your codebase.
And that’s just barely scratching the surface. It is the work of Evan Czaplicki from NoRedInk, which is quite amazing considering he works on it alone.
So if you’re tempted to take a crash course in Elm programming, then keep reading. Even if you do not intend to use it as a language, it contains interesting ideas that could still help you shape your applications, no matter the language you’ll write them in.
In this article we’re going to build a (somewhat) small application with Elm, learn piece by piece how it works and what the Elm architecture is, and to conclude we’ll draw some parallels with your usual Redux application.
Setup
First things first, let’s install some stuff. Let’s start by checking that we have the correct version of things. We’ll need Node and NPM (or Yarn if you’re cool):
$ node --version
v10.5.0
$ npm --version
6.1.0
Once this is done, let’s initialize a Node project in an empty folder, by doing npm init --yes
to create a package.json
.
Now that we have a standard setup, let’s install Elm. The easiest way to do so, is to simply run npm install elm --save
, and then do elm --version
to check that it properly worked:
$ elm --version
0.18.0
If you type elm
, you will notice that the binary is actually a placeholder for the whole Elm platform, which means you will actually never call elm
itself but the tools it exposes (eg. elm package install
, elm make
, etc.).
Note: the binaries will be placed in
node_modules/.bin
. If you don’t have it in your $PATH it is recommended to do so. The rest of this article will not use the full paths to each binaries but call them directly instead
First contact with Elm
Primitives
One of the things in the Elm tool belt is a REPL: an interactive console in which you can play with the language. So let’s fire it up and learn the core syntax of the language a bit. You launch the REPL by calling elm repl
, and you quit it by typing :quit
. Since this is our first contact, let’s just review some primitives and see how it goes.
The most basic types in Elm, as in most languages, are strings and numbers. If we type “foobar” (double quotes only) in the REPL this is what we get:
> "foobar"
"foobar" : String
As you can see, we gave Elm a string, and it returned something in the form of x : y
. This is a type annotation, this is how you declare what things are in Elm. In this case, Elm saw "foobar"
and basically said “That’s a string”.
If we try with a number we get the same result:
> 5
5 : number
Here we typed 5 and it said “This is a number”. Note that number
is lowercased because it is an alias for “Either a Float
or an Int
“. As mentioned before, Elm is strictly-typed as opposed to the language it compiles to (Javascript) which is loosely typed.
If you’re unsure what that signifies, it means that in Javascript the engine guesses your intentions and will let you do whatever you want on the basis that it can never be sure of them. If you want to try to do {foo: "bar"} + 2
it’ll trust your judgement and just let you do it, even though it does not make sense or might crash your app. Whereas in a strictly-typed language, you usually have to declare what things are and what they do and the engine will not let you do things that do not make sense in the context of what you’ve declared. As a small example of what that means, and try to add a number to a string:
> "foo" + 3
-- TYPE MISMATCH
The left argument of (+) is causing a type mismatch.
4| "foo" + 3
^^^^^
(+) is expecting the left argument to be a:
number
But the left argument is:
String
As you can see, we tried to do something that the compiler thought didn’t make sense, and it prevented us to. All good. Let’s proceed and declare our first function:
> add x y = x + y
: number -> number -> number
This piece of code is most likely going to be confusing in some aspects so let’s break down some of the confusion:
- First: Elm does not have any variable or function declaration keyword (like
const
,function
orlet
), you simply writesomeFunction = its contents
oruserAge = 3
and Elm will know what you meant and scope everything nicely. - Second: returns are implicit, you never type
return XXX
, because everything you do returns something. This is part of the fact that in Elm all things are immutable: you never modify an object or a variable, you simply create a new “version” of it with a new value. If you’ve had to fight with references in JS or were forced to useimmutable.js
and such to avoid your app behaving madly, this is already a huge relief in itself. - Third, you won’t use parenthesis in Elm in most of the places you’d use them in Javascript. They still have their standard use, but we just need them a lot less. Per example if we wanted to call our function, instead of doing
add(1, 2)
we’d simply doadd 1 2
:
> add 1 2
3 : number
Finally, Elm returned what it thinks the type of our function is and it properly guessed that we were expecting numbers. How? Because operators do not work the same in Elm as they do in Javascript.
In JS, operators are reused between types, what I mean by this is that you can do 5 + 5
but you can also do "foo" + "bar"
. In Elm, each type has its own operators, so you’d still do 5 + 5
but you’d do "foo" ++ "bar"
. The reason is that in Elm (as in a lot of functional languages) operators are functions as well (infix functions to be precise) and +
is just a function expecting two numbers, and not strings, whereas ++
is a different function that expects strings, not numbers.
If you looks closely at the type annotation of our add
function, you notice a new operator, ->
, it’s used to demonstrate arguments and return values. If per example we had a function that took two strings and returned a number we’d have String -> String -> Int
. You may wonder why the same operator is used for arguments and return types, and it’s quite simple: in Elm, absolutely all functions are curried by default. If you’re not familiar with currying, first of all no relation to the indian dish, second of all here is the definition for it:
In mathematics and computer science, currying is the technique of translating the evaluation of a function that takes multiple arguments (or a tuple of arguments) into evaluating a sequence of functions, each with a single argument
You may have encountered it a lot of times in Javascript without being aware of it. Per example here is a curried function:
function add(firstNumber) {
return function with(secondNumber) {
return firstNumber + secondNumber;
}
}
// Or with short arrow syntax in modern versions of ES
// const addWith = firstNumber => secondNumber => firstNumber + secondNumber
const addWithTree = add(3);
addWithTree(5) // 8
add(3)(5) // 8
Basically instead of having a function taking arguments A B C, you have a function taking an argument A, returning a function taking argument B, returning a function taking argument C, returning the result. In Elm, this is the default behavior of all functions, let’s try it with our basic add
function:
> add 1
: number -> number
Here we only specified one of the arguments of our function, and what we got back as we can see in the return type, is now a function that accepts one number, and returns another number (the result):
> addWithOne = add 1
: number -> number
> addWithOne 2
3 : number
This is the reason why arguments and return types both use the same operators: because Elm does not make the distinction.
Arrays (Lists)
Arrays (called Lists in Elm) work pretty much the same way in Javascript with one difference since we’re in typeland here: an array can only hold values of the same type. That is to say you never just have a list, you have a list of something:
> [1, 2]
[1,2] : List number
> [1, "foo"]
-- TYPE MISMATCH
The 1st and 2nd entries in this list are different types of values.
5| [1, "foo"]
^^^^^
The 1st entry has this type:
number
But the 2nd is:
String
Other difference with Javascript: types are not objects, so you won’t be able to do [1, 2].length
. Instead, you’d call the List.length
method, on the list. Same is true for all the functions you’re used to (map, filter, etc)
> someArray = [1, 2]
[1,2] : List number
> List.length someArray
2 : Int
> double someNumber = someNumber * 2
: number -> number
> List.map double someArray
[2,4] : List number
That doesn’t mean that this is the only way to do things as Elm supports a good bunch of function composition/applications operators, but we’ll see that later on, don’t worry about it.
Objects (Records)
Now that’s fine and all but if you’re coming from Javascript then you know that objects is where it’s at, so let’s create an object (called Record in Elm). The syntax is slightly different but familiar enough that you shouldn’t get lost:
> myObject = {foo = "bar", baz = 5}
{ foo = "bar", baz = 5 } : { baz : number, foo : String }
Only difference with JS is that we use =
instead of :
(since :
is for type annotations) but apart from that, standard rules apply, so you can call myObject.foo
and you will get "bar"
as you’d expect. There is however one tiny but useful difference: the dot operator is also a function (like everything in Elm):
> myObject.foo
"bar" : String
> .foo
: { b | foo : a } -> a
If we call .foo
by itself, Elm creates a function that will accept an object having a foo
key, and return its value. In the type annotation you may notice a
and b
, they’re basically placeholders for stuff Elm doesn’t know about, so you can pretty much read it like that:
> .foo
: { ¯\_(ツ)_/¯ | foo : ¯\_(ツ)_/¯ } -> ¯\_(ツ)_/¯
Now since .foo
is a function, we can pass our object as argument, and get the correct value:
> .foo myObject
"bar" : String
This may not seem very fascinating but it is crazy useful for composition, as we’ll see later.
One of the major differences between Elm and Javascript is that we cannot access fields that do not exist:
> myObject.nope
-- TYPE MISMATCH
`myObject` does not have a field named `nope`.
6| myObject.nope
^^^^^^^^^^^^^
The type of `myObject` is:
{ baz : number, foo : String }
Which does not contain a field named `nope`.
Hint: The record fields do not match up. One has baz and foo. The other has
nope.
That means no null
, no undefined
, etc. Again, Elm is about predictability, and null
is one of the major pitfalls of modern applications. The language goes very deep into avoiding anything unexpected ever happens and this is part of it.
I want to get off Mr. REPL’s wild ride
Ok we’ve played enough in the REPL, this isn’t what we’re here for, let’s CODE SOME STUFF. Let’s start by creating a Main.elm
file, within a src/
folder. Note that the name of that file has no particular significance, Main.elm
is just kind of a “convention” amongst Elm developers, but we could have named if Foobar.elm
with the same results.
Before writing anything, let’s make the file empty for now, and boot it up. The Elm platform comes with a development server called elm reactor
so let’s type that and navigate to the URL it’ll give you, which should show something like this:
Click on the src
folder then on Main.elm
. If you do so, Elm will tell you that it expected literally anything from you and that you gave it an empty file so it’s shaking its head in disappointment:
-- SYNTAX PROBLEM
Main.elm
I ran into something unexpected when parsing your code!
I am looking for one of the following things:
"{-|"
a definition or type annotation
a module declaration
a port declaration
a type declaration
an import
an infix declaration
whitespace
“Even whitespace dude… just… just type some spaces, seriously, anything”
Setting up our web application
As I mentioned in the introduction, Elm is not a generic language, it is a language to build web applications. Which means that the framework is built into the language. And in this case, to run the application, Elm is expecting us to have a main
function that will return some HTML. In a way main
is the entry point – if you’re familiar with C it’s the same idea – so let’s add it to our file:
src/Main.elm
main = "Hello world"
Refresh the page (the reactor will recompile by itself in the background), and you’ll see… another error. This time, Elm is saying that it wanted you to give it HTML or SVG or something, but you gave it a string which is not a webapp so he’s starting to be really pissed off.
Indeed, Elm does not consider strings to be Html elements in and of themselves. Instead we need to give Elm an HTML Text
node, which you can find in the Html
module.
To import a package in Elm, the syntax is a bit different to what you’re used to but not that far off, we’re going to call import Html
, and from that module we’re going to use the text
function which renders an HTML text node:
import Html
main = Html.text "Hello world"
Now in Elm when you import a package, by default it imports it as itself, which is why we call Html.text
. It is the equivalent of the following code in Javascript:
import Html from "html";
const main = Html.text("Hello word");
But we can also import only the relevant parts, and expose them in the current scope. For this, we use the import X exposing Y
syntax. In this case, we want to import Html
and expose text
. Once a function is exposed, it means that it is available in the current scope as itself:
import Html exposing (text)
main = text "Hello world"
Again translated to Javascript:
import Html, { text } from "html";
const main = text("Hello word");
If you refresh the page, your application should now actually be returning HTML and you should see the gloriousness of your Web 5.0 page.
Writing some actual HTML
Ok, we now know how to write some text to a page, but what about HTML?
In Elm, since everything is a function, you also write your HTML with functions. The same way you do in Javascript with packages like hyperscript or the same way React does underneath the JSX syntax.
Every element is a function that takes two arguments: its attributes, and its children. Let’s wrap our text in a div per example:
src/Main.elm
import Html exposing (text)
main = div [] [text "Hello world"]
Here as I explained, we are calling the div
function with two arguments: its attributes (which there are none, so we just pass an empty list []
) and its children (our text, also within a list). If you’re confused with this bit, remember that they’re just arrays, we could per example have this:
main = div [] [text "Hello world", text "Some other text"]
If you try to refresh your page, Elm will complain that it doesn’t know where div
comes from:
Cannot find variable `div`
3| main = div [] [text "Hello world"]
^^^
Maybe you want one of the following?
min
pi
sin
Html.div
It does however try to guess what we meant, in this case we indeed meant Html.div
. Now we could rewrite our example as main = Html.div bla bla
, or we could also update our import to show exposing (text, div)
. But, there are like a hundred elements in HTML and nobody got time to remember them all so let’s import all the elements:
src/Main.elm
import Html exposing (..)
main = div [] [text "Hello world"]
The (..)
syntax is used to signify “expose all”, Javascript doesn’t really have an equivalent but it isn’t that far from doing that:
import {text, div, html, body, etc and so forth for all elements} from 'html';
This may look dangerous but in Elm everything is a module, there is no global context, so while you’re exposing a lot of functions into the scope, you’re only doing so in that file. If you go into another file and try to call text
as is, it won’t work.
If you refresh your page, you should see “Hello world” again, which is good, but let’s try to add a class to it. As said before, the first argument to any HTML function is a list of attributes, but what is an attribute? If you answered “Also a function” then you are correct, cause this is Elm and everything is a function, even me, even you, we all functions now. So to apply a class to an element, we call the class
function, imported from Html.Attributes
:
import Html exposing (..)
import Html.Attributes exposing (class)
main = div [class "container"] [text "Hello world"]
If you refresh your page, well, you won’t see the difference but if you look in your inspector of choice we do see that our class was properly applied:
Compiling our application
What we typed before is some HTML, but we’re not gonna code our HTML context like that (I’m talking about the head, body, doctype, etc). Instead we’re going to embed that HTML onto an existing element. If you’re familiar with React this is the exact same mechanism that when calling ReactDOM.render
: everything we do and render will be rendered within an existing HTML element on an existing page.
Let’s create a basic index.html
and place it into a public
folder:
public/index.html
MY COOL ELM APP
The endgame here is to embed our application onto that main
element. So first, we’ll need to compile our Elm code to a main.js
file since that’s what we included here. To do so, we’re going to use another tool from the elm platform: elm make
:
$ elm make src/Main.elm
Success! Compiled 1 module.
Successfully generated index.html
If you run this, you’ll notice that his is not quite what we want: Elm generated an index.html
of his own with all the Javascript thrown in like a madman. We want a main.js
instead so we need to specify the format:
$ elm make src/Main.elm --output public/main.js
Success! Compiled 1 module.
Successfully generated public/main.js
There, now we have our javascript file compiled. Remember how I said we needed to embed our application onto an element? If you open the public/index.html
file in your browser, as it is, you actually already have access to what we need:
As you can see, Elm defined a global Elm
object onto the window
. Inside it is our Main
module (since our file is called Main.elm
), and that module has two methods:
embed
is what we want, and accepts the arguments we’d expect it tofullscreen
is basically just an alias forembed(document.body)
(sort of)
You can see a mention of flags
here but we’ll see that later. Ok so let’s write the code we need in our HTML file:
index.html
Refresh and miracle, here is our Hello World. Do note that elm make
has no --watch
flag, it is meant for basic compilation. We’ll setup an actual build tool later on but for now let’s just define a simple NPM script to do that for us. Open up your package.json
file and add a build
script to do what we just did:
package.json
{
"name": "elm-blogpost",
"scripts": {
"build": "elm make src/Main.elm --output public/main.js"
},
"dependencies": {
"elm": "^0.18.0"
}
}
Try to run npm run build
and if everything went well it’ll say “Compiled 1 module” which is good. On that note, I think that’s enough tinkering, let’s go back to coding some Elm!
The Elm architecture
Now we have an actual page, and an actual Elm file to work with, let’s jump right into it. For our first baby steps, we’re going to do something hella basic, we’re going to create a counter, with a + and – button. I know you want to go off right now and code your fantastic “Uber for Pretzels” app idea but you looked at me weird when I said +
was a function so trust me.
Every web application (not website) can be described in the following way:
- It has a state, holding information about itself
- It has actions, which describe stuff your application can do
- It has a way to accept these actions and return a new state
- It has a way to display said state
If you’re familiar with Redux then this should start sounding familiar right about now, because that is exactly what React/Redux is: the store is your state, reducers receive actions and return a new state, and React renders that state into HTML. Well guess what, it’s exactly the same in Elm: every Elm application is divided into these four parts:
- Model is your state, it’s a representation of the data your webapp can hold
- Messages are your actions, which are then sent to…
- Update is your reducer, it’s a function that receives a message and the state and returns a new state
- View is your… view? It receives the model and returns HTML/SVG
The model
Let’s start by the Model because that is the easiest part. We’re going to define a new type, called Model
, which we’ll use to tell Elm everywhere “This is what my data should look like”. There are two ways to create new types in Elm:
- By creating an actual type using the
type MyType = ???
- By creating a type alias using
type alias MyType = ???
We’ll go over the difference between the two and what each means in just a second.
In our case, the smallest possible representation our state could have, would be simply and Int
, so we would say type alias Model = Int
, because it’s just a counter, ain’t nothing else that we need to know. However the fact is that 99.84% of applications never have just “one” thing they need to be aware of so it is recommended to instead make your model an object:
src/Main.elm
import Html exposing (..)
import Html.Attributes exposing (class)
type alias Model = {counter : Int}
main = div [class "container"] [text "Hello world"]
That’s it, our state is a record, that has a counter
property, which is an int. Note that we used :
here, because this is a type annotation. We’re not declaring a variable, this isn’t an actual object it’s an alias for an object that has a specific shape that we want, kind of like that rock you found on the beach once and named Peter. If we create a new record {counter = 2}
, even if we don’t explicitly say that it’s an instance of Model, it still is because it quacks like a Model.
That’s the difference between a type and a type alias: a type alias is just a nickname we gave to another bunch of types, it’s virtual, it does not actually exist, if you pass something that sorta looks like it, it’s fine.
Whereas if you declare an actual type, then it’s real. It exists, if a function asks for MyCustomType
, then it better be an instance of one.
Underneath type alias Model = ...
we’ll also define our initial model: the default values the app will have when it starts. In our case, it’s just going to be setting the counter at 0:
type alias Model = {counter : Int}
model = {counter = 0}
The message
The message is a type (not a type alias) that represents anything that can happen to our application. In our case what can happen to our counter? It can increment or decrement, so let’s define that. We’ll use type Message = ???
and then we’ll enumerate what kind of messages our app will receive, separated by a |
:
src/Main.elm
import Html exposing (..)
import Html.Attributes exposing (class)
type alias Model = {counter : Int}
model = {counter = 0}
type Message = Increment | Decrement
main = div [class "container"] [text "Hello world"]
If you’re like me when I started learning Elm, your first reaction might be “Where the hell do Increment and Decrement come from?”. Nowhere, we just defined them: this is called a type union (or tagged union). Think of your message as an enumartion of arbitrary types, let’s boot up ye old Elm REPL to see what I mean:
> type Message = Increment | Decrement
> someMessage = Increment
Increment : Repl.Message
> someOtherMessage = Decrement
Decrement : Repl.Message
> Message
Cannot find variable `Message`
As you can see, if we create a variable myMessage
and assign it the value Increment
, then Elm knows that myMessage
is of the Message
type. But we cannot create directly a Message because it can mean multiple things.
Think of it like this: 5 and 10 are both Int
s, but you cannot create an Int
without knowing the value, you can’t do like someNumber = Int
you have to say what number and do someNumber = 5
.
This is the same mechanism here, both Increment
and Decrement
are examples of Messages, but you cannot just straight up create a Message without saying what kind of message.
The update
As I mentioned before, reducers in Redux were inspired by updaters in Elm, most of the time a reducer in Redux looks like that: it receives the current data of the application (model) and what to do with it (message). It then decides how to update the data depending on the type of the message.
It can be done any number of way (with if
s and else
s) but usually it is done through a switch
:
function reducer(model, message) {
switch (message.type) {
case "INCREMENT":
return model + 1;
case "DECREMENT":
return model - 1;
default:
return model;
}
}
Then you would call that function on the state, with a particular message:
reducer({ counter: 0 }, { type: "INCREMENT" }); // {counter: 1}
Well that’s exactly what an updater is in Elm. No seriously, I mean this is exactly what an updater is, with the switch and all. Let’s start by declaring our function, like in redux it accepts our model and a message, and since we don’t know what to return yet, let’s just make it return the model as is for now.
update message model = model
Before we proceed, let’s talk about type annotations. Elm is very smart at figuring out the types of stuff, so chances are if you used this update function in the wild with enough context it’ll figure out that model
is a Model
and message
is a Message
. That being said, Elm can only cover your ass so much so it’s best practices to always define your types. We’ve seen the syntax used to do this before (variable : argument -> other argument -> ... -> whatever is returned
) but where do we put that? Simply enough, right above:
update : Message -> Model -> Model
update message model = model
Little recap if you’re lost: we basically said “update is a function that accepts a message, then it accepts a model, and when it’s done accepting things, it’ll return a model”.
Now this is nice and everything, but we’re not doing anything of our message
variable so let’s write a switch. The syntax for a switch in Elm is (again) relatively similar to Javascript but also kind of not the same:
case someVariable of
String -> "what to return if someVariable is a string"
Int -> "what to return if someVariable is an int"
_ -> "what to return by default otherwise"
As you can see, I’m abusing the word “switch” a bit here as we’re not switching on the value (like in JS) but on the type. Also as a reminder, returns are implicit. With that information in mind, we want to write a switch on message
, and increment model.counter
if it’s Increment
, and decrement it if it’s Decrement
. Let’s start by Increment
:
update : Message -> Model -> Model
update message model =
case message of
Increment -> ??? return a new model here ???
Ok, what do we put here? Our counter int
is on a property of the Model record, that means that we cannot just do Increment -> model.counter + 1
because that would just return something like “2” and we want to return a new Model.
So we have to update the property itself on the record and then return said record. To accomplish that in Javascript, without breaking immutability, you would do the following:
switch ((action.type, model)) {
case "INCREMENT":
return { ...model, counter: model.counter + 1 };
}
As usual, syntax slightly differs in Elm but should be familiar enough for you to not get lost too much:
update : Message -> Model -> Model
update message model =
case message of
Increment -> {model | counter = model.counter + 1}
The idea is the same: we create a new object, then we “copy” our previous object’s properties on it through |
so that everything that was in the old model is copied onto the new one, and then we update the property on our new model.
Do note that counter
is the only property we have on Model
so far, so this syntax has currently no benefits over doing this:
update message model =
case message of
Increment -> {counter = model.counter + 1}
This is just so you get used to the idea of creating new records from other records because it is very handy.
Let’s stop there briefly, and try to compile. Run npm run build
in your console, and you’ll get the following message:
This `case` does not have branches for all possibilities.
9|> case message of
10|> Increment -> {model | counter = model.counter + 1}
You need to account for the following values:
Decrement
Add a branch to cover this pattern!
Elm saw that our application could do two things: increment and decrement. But we only told it what happened when we increment, so it refuses to compile because not all possibilities are accounted for. This is something you’ll encounter a lot in Elm, as I mentioned before it will try to shield you from unexpected and will refuse to compile if you do not account for possible errors. So let’s write our Decrement
branch. If you’re with me so far, it’s pretty straightforward, we just have to write the same thing with a -
instead of a +
:
update : Message -> Model -> Model
update message model =
case message of
Increment -> {model | counter = model.counter + 1}
Decrement -> {model | counter = model.counter - 1}
And there we go, we now have an update
function that takes into account every possible thing that could be happening in our application.
You might say “Where is the default branch? All good switches have a default branch right?” and you’re correct but not in Elm. Because we know that Message can only ever be one of those two types, we don’t need to add a default branch because we’re absolutely certain that that will never happen. Because if there was any possibility for Message to be something else, the app would not have compiled. It’s as simple as that.
The view
We saw briefly how to conjure up some HTML before, so this is going to go a bit faster. What we want is:
- Two buttons, a + and a –
- The current count
Laying the foundations
Let’s define a view
variable and lay the HTML foundations of that:
view =
div [] [
button [] [text "-"],
text "5",
button [] [text "+"]
]
Remember that the first argument to all HTML functions is a list of attributes (excepted text
because a text node has no attributes). The second is a list of children, so we created a div
with three children: button, text, button
.
Something important to note: Elm, just like React and other libraries, will not let a variable or a function return a bunch of elements without a parent node. You only ever return one element, which can then have as many children as you’d like. This is why we wrapped everything in a div
.
Let’s check in the browser see if we’re ok. Now if you remember, the entry point of our app is the main
variable so for now let’s just say main = view
:
src/Main.elm
import Html exposing (..)
import Html.Attributes exposing (class)
type alias Model = {counter : Int}
model : Model
model = {counter = 0}
type Message = Increment | Decrement
update : Message -> Model -> Model
update message model =
case message of
Increment -> {model | counter = model.counter + 1}
Decrement -> {model | counter = model.counter - 1}
view =
div [] [
button [] [text "-"],
text "5",
button [] [text "+"]
]
main = view
Run npm run build
, then open our public/index.html
file in your browser and there we go, wonderfully disgusting:
Using our data
For the actual text, we currently hardcoded “5” but let’s use the data from our model. In order to do so, we’re going to make view
a function that accepts a Model
and returns some Html
. This will allow us to use model.counter
to display the proper count. If you remember, to define arguments we write view model = ...
:
view model =
div [] [
button [] [text "-"],
text model.counter,
button [] [text "+"]
]
We can then call our view
as a function with the model
variable we defined earlier to display it:
main = view model
If you try to run npm run build
at this stage, you’ll get an interesting error:
-- TYPE MISMATCH
The argument to function `view` is causing a mismatch.
28| view model
^^^^^
Function `view` is expecting the argument to be:
{ counter : String }
But it is:
Model
Why? Because Html.text
expects model.counter
to be a string, but we passed 3
which is a number.
In order to fix this, we’ll use a core function of Elm called toString
which converts stuff to strings (I know, you’re flabbergasted). So let’s call it on our model.counter
variable in the view
function:
view model =
div [] [
button [] [text "-"],
text (toString model.counter),
button [] [text "+"]
]
Notice that we wrapped it within parenthesis to avoid the Html.text
function thinking that we passed it two arguments (as if we had done text(toString, model.counter)
instead of text(toString(model.counter))
).
Adding interactions
Now let’s make our buttons actually do something. For this we we’ll use another module from the Html
package: Html.Events
. Let’s import it and expose the onClick
function:
import Html.Events exposing (onClick)
Here onClick
is an attribute function (just like class
), ie. it returns an instance of Html.Attribute
. But what do we pass to it?
Remember when we defined our Message? We said “A message is either Increment
or Decrement
” – well that’s exactly what we need here. We’re passing the type itself as argument to onClick:
view model =
div [] [
button [onClick Decrement] [text "-"],
text (toString model.counter),
button [onClick Increment] [text "+"]
]
While we’re at it, let’s add a type annotation to our view
function. We know it accepts a Model
and returns some Html
so let’s naively try that:
view : Model -> Html
view model = ...
If you try to build the app, here is what Elm tells you:
-- TOO FEW ARGUMENTS
Type Html.Html has too few arguments.
18| view : Model -> Html
^^^^
Expecting 1, but got 0.
This one is very tricky to grasp at first, so let me walk you through what the problem is. We told Elm that our app returned Html, but if you goto the definition of HTML in your editor/IDE of choice, here is what you will see:
type alias Html msg = VirtualDom.Node msg
We read that it’s expecting to know what kind of message will this particular piece of HTML propagate. We know this! It’s going to be a Message
, so let’s pass that as argument to Html
:
view : Model -> Html Message
view model = ...
This is where you might start to get lost but this is just generics. If you’re familiar with them, this would be the equivalent Typescript annotation:
const view: Model => Html =
function(model) { ... }
I’m not gonna lie I don’t know the specifics of why this is necessary (internal optimization I’m assuming) but when defining the type of a function returning HTML, you need to specify what kind of message it can possibly send. Usually you only have one Message
per app so most of your annotations will look the same (you can usually omit them for most HTML functions unless they have specific arguments and such).
Now that this is taken care of, if you look at this piece of code from afar, you may notice that we’re repeating ourselves here (I know it’s like three lines but imagine this is a real app). How do we organize our logic into reusable pieces? MOAR functions, that’s goddamn right, hide the look of surprise on your face.
Let’s define a counterButton
function. Think about what we need to pass to it: we need to tell it what message it’ll send, and what its text will be, so something like counterButton message label = ...
. Easily enough, we can do just that to split our view into reusable parts:
view : Model -> Html Message
view model =
div [] [
counterButton Decrement "-",
text (toString model.counter),
counterButton Increment "+"
]
counterButton : Message -> String -> Html Message
counterButton message label =
button [onClick message] [text label]
Always remember: “It’s just functions”. There’s no black magic, no special syntax to follow for this to work, it’s all just functions calling other functions, all in the family, Lannister style. This is one of benefits of Elm (and functional languages in general): while the initial syntax barrier requires some work to overcome. Once you’re through, there is no difficulty curve, it’s smooth sailing.
Also note that the order of functions does not matter. We defined counterButton
after the function using it, since it’s all compiled, there’s no worry to have about that. We could put our main
all at the top before anything’s defined and it would still work.
Joining all the parts together
Great, so we have a model, an updater/message, and a view. How do we make it all work together though? You’ll notice if you try to build our current application and click on a button, nothing happens.
To bind the pieces of our app, we need to return a Program
. A program is a special type defined internally in Elm that describes how an application behaves.
You could create a Program yourself but Elm also comes with some examples of programs directly into the Html
package, which means that by doing import Html exposing (..)
we actually already brought some program functions into scope. There are three of them:
Html.beginnerProgram
is oriented at newcomers and ask of you a model, view and update (this is what we’re going to use)Html.program
asks the same, but also allows you to use subscriptions and commands (which are used to manage events and side effects)Html.programWithFlags
is the same thing as above but with the additional benefit that you can pass it some data from Javascript to initialize it. You could per example already have some data in JS that you would pass Elm to create your initial model, or some environment variable that you’d like Elm to know about, or to dispatch some events, stuff like that.
We’ll use the first one for now. There’s only one issue, what do we pass to it? Well if you ever ask yourself that, remember that you can always check out what kind of arguments we need to pass to something, directly in elm repl
:
> import Html
> Html.beginnerProgram
: { model : model
, update : msg -> model -> model
, view : model -> Html.Html msg
}
-> Platform.Program Basics.Never model msg
Ok so Html.beginnerProgram
is a
, it accepts a record with three keys: a model, an update, and a view, and then it returns a Program
.
If you think about it, we actually already have all we need ready to be used: we have a model
variable, an update
function and a view
function, we just need to create a record from these three variables, so let’s do just that. We’re basically going to just assign each variable to its corresponding key on a record:
main = beginnerProgram {model = model, update = update, view = view}
This is just a basic record, if we had named our view variable makeMyView
then it’d be beginnerProgram {view = makeMyView, ...}
etc. You don’t have to name your variables update
, model
and such, it’s just good practices.
Run build again and refresh your browser and there we go:
Uber for Pretzels, here we come
Before we move on to the next part, let’s cleanup our file a bit. You know what it needs? Comments, everything is always better with more comments. And how do you comment in Elm? With -- foobar
for single line comments, and {- foobar -}
for multiline, example:
-- This code adds 1 and 1
addStuff = 1 + 1
{-
This code adds 1
and 1
wow
much numbers
-}
addStuff = 1 + 1
So let’s separate a bit the pieces of our file with some comments to clearly delimitate the various sections of it:
import Html exposing (..)
import Html.Attributes exposing (class)
import Html.Events exposing (onClick)
-- Model
type alias Model = {counter : Int}
model : Model
model = {counter = 0}
-- Update
type Message = Increment | Decrement
update : Message -> Model -> Model
update message model =
case message of
Increment -> {model | counter = model.counter + 1}
Decrement -> {model | counter = model.counter - 1}
-- View
view : Model -> Html Message
view model =
div [] [
counterButton Decrement "-",
text (toString model.counter),
counterButton Increment "+"
]
counterButton : Message -> String -> Html Message
counterButton message label =
button [onClick message] [text label]
-- Main
main = beginnerProgram {model = model, update = update, view = view}
It’s not much but it doesn’t hurt and it’ll help us see a bit more clearly so, yay.
Setting up a build system
Before we move on, let’s setup an actual build system so we don’t have to keep running npm run build
all the time. There are various bindings for Elm (Gulp, Grunt, etc) but since I love me some chunks let’s use Webpack. If you’re not familiar with it, we have a great article about it written surely by someone very smart I guess whoever he is.
Before we begin, here’s what you should have in terms of files and folders:
├── elm-stuff
├── node_modules
├── public
│ └── index.html
├── src
│ └── Main.elm
├── package.json
└── elm-package.json
Setting up a basic Webpack build
We’ll need two things at first, Webpack and the Elm loader:
$ npm install webpack elm-webpack-loader --save-dev
Let’s split the Javascript that we hardcoded in public/index.html
into its own file named index.js
, and place it inside src/
as well. We’ll need to get the Elm
variable from somewhere, and the Elm loader is going to give us just that when importing an Elm file so let’s add an import for our Elm file at the top:
src/index.js
const Elm = require("./Main.elm");
Elm.Main.embed(document.querySelector("body"));
Now let’s create our Webpack configuration, it’s going to be very basic:
webpack.config.js
module.exports = {
entry: "./src/index.js",
output: {
filename: "main.js",
path: __dirname + "/public",
},
module: {
rules: [{ test: /\.elm/, use: "elm-webpack-loader" }],
},
};
Here’s what it says if you’re not familiar:
- The file we want to compile is
./src/index.js
- We want to compile it to
public/main.js
- We want to load all
.elm
files we find with theelm-webpack-loader
package.
Next we’ll setup a plugin to automatically create our public/index.html
file with the correct script
tag to the built assets. For this we’ll need a plugin:
npm install html-webpack-plugin --save-dev
There’s no need to configure anything besides the page title which we’ll pass through an options object:
const HtmlPlugin = require("html-webpack-plugin");
module.exports = {
// [...]
plugins: [new HtmlPlugin({ title: "MY COOL ELM APP" })],
};
Let’s give it a shot, run webpack
and if it all worked here is what you will get:
$ webpack
Hash: 2fbe3770b313e328dae9
Version: webpack 3.10.0
Time: 1188ms
Asset Size Chunks Chunk Names
main.js 201 kB 0 [emitted] main
index.html 184 bytes [emitted]
[0] ./src/index.js 84 bytes {0} [built]
[1] ./src/Main.elm 198 kB {0} [built]
Child html-webpack-plugin for "index.html":
1 asset
[2] (webpack)/buildin/global.js 509 bytes {0} [built]
[3] (webpack)/buildin/module.js 517 bytes {0} [built]
+ 2 hidden modules
The plugin will have generated a new public/index.html
file with the correct script tag so that’s one less thing to worry about, which is always nice.
Setting up hot reloading
One of the advantages of Webpack is that it supports hot reloading (ie. “patching” the current page with new code without having to reload it). Setting up is fairly straightforward, we’ll use Webpack’s dev server and the relevant Elm plugin:
npm install webpack-dev-server elm-hot-loader --dev
To set it up, we just need to slightly amend the loader in our Webpack configuration:
{test: /\.elm/, use: ['elm-hot-loader', 'elm-webpack-loader']},
This is the final file:
const HtmlPlugin = require("html-webpack-plugin");
module.exports = {
entry: "./src/index.js",
output: {
filename: "main.js",
path: __dirname + "/public",
},
module: {
rules: [
{ test: /\.elm/, use: ["elm-hot-loader", "elm-webpack-loader"] },
],
},
plugins: [new HtmlPlugin({ title: "MY COOL ELM APP" })],
};
Now, let’s add a script to our package.json
to boot up a development server, and while we’re at it let’s also update our old build
script to simply call webpack
:
package.json
{
"name": "elm-blogpost",
"scripts": {
"build": "webpack",
"start": "webpack-dev-server --inline --hot"
},
"dependencies": {
"elm": "^0.18.0"
},
"devDependencies": {
"elm-hot-loader": "^0.4.2",
"elm-webpack-loader": "^3.0.6",
"html-webpack-plugin": "^2.24.1",
"webpack": "^1.13.3",
"webpack-dev-server": "^1.16.2"
}
}
Let’s see if it all works, run npm start
in your terminal and navigate to http://localhost:8080/ – you should see the application as we left it before. Now go into src/Main.elm
and try to change per example the label of one of the buttons.
If you look back in the browser, you’ll see that the page has been updated without any intervention from your part, and more importantly the state was left intact (ie it did not reset the counter). The page wasn’t refreshed, instead its code was updated live.
Advanced messaging and user input
Messages with arguments
Up until now our messages have been very basic. We communicated with our app one word at a time, “increment” or “decrement” or “groot” but this is rarely going to be the case in actual applications. More often that not, you’ll say something like “Hey app, please do X with Y”.
So how do we create messages that have arguments? Let’s try to add an IncrementBy
message to our app, allowing us to increment the counter by any number.
The first step is to update our Message
type to add the new possibility. We’re going to append IncrementBy
to it but we want it to have an argument which would be an Int
, so… that’s exactly what we’re going to write:
type Message = Increment | Decrement | IncrementBy Int
We would then create this message by doing per example incrementByFive = IncrementBy 5
just like calling a function.
If you still have Webpack running from the previous section, you should see an error popping up in the console telling you to add this new possibility to our update
function.
When a message has arguments, they’ll be received on the left-hand side of the switch condition, that means that where we had Increment -> ...
we’d have IncrementBy howMany -> ...
. With this in mind, it’s fairly easy to copy/paste the line above and adapt it to our needs:
update : Message -> Model -> Model
update message model =
case message of
Increment -> {model | counter = model.counter + 1}
Decrement -> {model | counter = model.counter - 1}
IncrementBy howMany -> {model | counter = model.counter + howMany}
Again, remember that this is all just functions. If we wanted to abstract away that feature under one, we could totally do that, per example:
incrementCounterBy : Model -> Int -> Model
incrementCounterBy model howMany = {model | counter = model.counter + howMany}
update : Message -> Model -> Model
update message model =
case message of
Increment -> incrementCounterBy model 1
Decrement -> incrementCounterBy model -1
IncrementBy howMany -> incrementCounterBy model howMany
Let’s keep it simple for now though and keep the repetition so we don’t spread out too much. Remember that we’re not building an actual application yet here, this is just for learning, in a real app there are dozens of simpler ways to do what we’re doing.
Anyway, we now have an IncrementBy
message which our update knows about. The compiler should run fine but we still need to add a way to dispatch said message.
For this we need to update our view with a new thing: a text input so the user can enter how much to increment by, and a button to do just that.
For the button, we don’t need to define anything else because we already defined a counterButton
function which we can reuse. You pass arguments to messages the same as for functions, example:
button [onClick (IncrementBy 5)] [text "Increment by 5"]
-- Or in our case
counterButton (IncrementBy 5) "Increment by 5"
Again, the parentheses are there to avoid onClick
thinking this is an argument, ie. we want onClick(IncrementBy(5))
and not onClick(IncrementBy, 5)
.
For the input, we will need somewhere to keep track of the current value held by the input, instead of getting it when dispatching the message. If you’re familiar with React, this is called a controlled input:
- An uncontrolled input has whatever value it wants, and we grab that value at the precise moment where we want to do something with it, using DOM voodoo or extracting it from a Javascript
Event
object. - A controlled input’s value is held somewhere else (state, redux, Model, can of tomato sauce, etc.) and the input always reflects that value, it is a projection of it. To use the value somewhere else, instead of grabbing it from the input, we instead directly use the value that we have stored aside and that the input itself used.
Uncontrolled input
document
.querySelector('input[name="foobar"]')
.addEventListener("click", function(event) {
// Do something with the value
console.log(event.target.value);
});
Controlled input
const value = 5;
// On every "loop" of your app, make sure input
// holds the correct value
document.querySelector("input").value = value;
// Do something with the value somewhere else
console.log(value);
Basically in a environment with controlled inputs you have a single source of truth which your inputs reflect, whereas with uncontrolled inputs they are the source of truth.
Now why am I talking about this: uncontrolled inputs are not a thing in Elm. Why? Because they’re uncontrolled and if there’s one thing this language dislikes it’s things it has no control over, Javascript being one of them. Think of Elm as a country with very tightly-guarded and closed borders, while JS is the country right next door: we don’t trust anything coming from them cause JS is a big ball of randomness and quirks. Hence why the recommended design is to use controlled inputs.
All that to say: we need to update our Model
to keep track of the input value. We know that it will be a string, so we just need to add that. You can name it however you want, we’ll name it incrementBy
to be explicit:
type alias Model = {counter: Int, incrementBy: String}
Then we need to create a new message, to update that value. Again, naming is up to you, we’ll go with SetIncrementBy String
:
type Message = Increment | Decrement | IncrementBy Int | SetIncrementBy String
Once this is done, the compiler will remind you to add this new possibility to your update
function, using the same pattern as for IncrementBy
:
update : Message -> Model -> Model
update message model =
case message of
Increment -> {model | counter = model.counter + 1}
Decrement -> {model | counter = model.counter - 1}
IncrementBy howMany -> {model | counter = model.counter + howMany}
SetIncrementBy value -> {model | incrementBy = value}
It will also tell us that we need to update our model
variable (which is our initial state) as currently Elm has no idea what to use as default for incrementBy
so let’s just give him an empty string:
model = {counter = 5, incrementBy = ""}
Now all we have to do is create our input. We can use the input
element from the Html
package like we did for div
and such. As for the event to dispatch our message on, we’ll use onInput
which means “whenever the value of the input changes”. You’ll notice that I won’t be passing any argument to SetIncrementBy
because onInput
will already be passing the current value to whatever message we specify (just like onChange
does in React):
input [type_ "number", onInput SetIncrementBy] []
Per example if we typed “foobar” then SetIncrementBy
would be called with the argument "foobar" : String
, simple enough.
You’ll also notice that I used type_
and not type
, because type
is a reserved keyword (ie. if the language uses it so you can’t use it yourself). So the Html
package instead exports the attribute as type_
. This is a convention in Elm and will be the same for any reserved keyword. Since we aren’t exposing all attributes from Html.Attributes
, we’ll need to add it to our import, same for onInput
:
import Html.Attributes exposing (class, type_)
import Html.Events exposing (onClick, onInput)
The reason we don’t just change those to exposing (..)
is that HTML has a lot of attributes and doing so would prevent us from being able to name our variables whatever we want since half the words in the english language are HTML attributes.
Now let’s put all this happy bunch together:
view model =
div [] [
counterButton Decrement "-",
text (toString model.counter),
counterButton Increment "+",
counterButton (IncrementBy 5) "Increment By 5",
input [type_ "number", onInput SetIncrementBy] []
]
Perfect! Or almost, you’ll notice that our custom increment is still hardcoded to 5
which isn’t good is it? Instead we want to use the value specified on the input. Luckily we know that it’s on our Model so we can access it, the same way we did to display the counter, by using model.incrementBy
:
counterButton (IncrementBy model.incrementBy) "Increment By 5",
We’ll also need to update our text to show the actual value we’re passing. For this we’ll use the string concatenation operator that we saw in the introduction – ++
– to form the final string, like this:
counterButton (IncrementBy model.incrementBy) ("Increment By " ++ model.incrementBy),
Notice how we also wrapped the string in parenthesis, this isn’t mandatory in this case but it’s good practices to do so to improve code clarity. If the compiler is still running in your terminal (if not type npm start
again) you’ll see that it’s telling us the following thing:
The argument to function `IncrementBy` is causing a mismatch.
32| IncrementBy model.incrementBy)
^^^^^^^^^^^^^^^^
Function `IncrementBy` is expecting the argument to be:
Int
But it is:
String
Well that makes sense doesn’t it? Our input value is a string, and IncrementBy
wants a number. Fair enough. There are two ways to solve this:
- Make
model.incrementBy
anInt
by converting it as such on reception ofSetIncrementBy
- Convert
model.incrementBy
within our view
In this case, the second option is recommended: we don’t know if we’re gonna want to do something else with our input value later on maybe that would require it to be a string. We just know that we need it to be an int for this particular case, so it’d make sense to only convert it here.
But how do we convert a string to an int? You’re probably thinking “Well we just call toInt
or something on it” and you’d be close but yet so far. You’ll see what I mean in a few seconds but first, let’s adapt our view
variable to let us prepare our data accordingly before rendering the view.
Data preparation
If you remember, earlier I said that everything in Elm returns something, and while this is generally true by default there is however a construct that you can use to prepare data before acting on it. This construct is called let in
. Let’s see how to use it in a small abstract example, imagine that we have a function accepting two strings and we want to return them joined:
> joinStrings "foo" "bar"
"foobar" : String
We could write it in the following way:
joinStrings : String -> String -> String
joinStrings first second = first ++ second
But if (for some reason) we wanted to prepare our final string before returning it we could also write it like so:
joinStrings : String -> String -> String
joinStrings first second =
let joined = first ++ second
in
joined
This may seem rather hard to parse at first, but you have to read it in a Javascript let
kind of sense. In JS you would do something like this:
function joinStrings(first, second) {
let joined = first + second;
return joined;
}
And just like in Javascript we can declare multiple things with a single let:
foobar : String -> String -> String
foobar foo bar =
let
baz = foo ++ "foo"
qux = bar ++ "bar"
in
baz ++ qux
> foobar "foo" "bar"
"foofoobarbar" : String
Take your time to read this because this is a very important piece of Elm, you’ll need it in a lot of occasions. If you’re still having some trouble, let’s try to apply it to our little app here. We’d declare a numberInput
variable where we’d convert model.incrementBy
to an Int
, using a function from the String
module called String.toInt
. Then we’d use it in our view, in the in
section:
view : Model -> Html Message
view model =
let
numberInput = String.toInt model.incrementBy
in
div [] [
counterButton Decrement "-",
text (toString model.counter),
counterButton Increment "+",
counterButton (IncrementBy numberInput) ("Increment By " ++ model.incrementBy),
input [type_ "number", onInput SetIncrementBy] []
]
Note that we still use model.incrementBy
in the button’s label because in this case, we do want a string.
Failures and results
Now try that and you’ll get a rather cryptic error message:
The argument to function `IncrementBy` is causing a mismatch.
34| IncrementBy numberInput)
^^^^^^^^^^^
Function `IncrementBy` is expecting the argument to be:
Int
But it is:
Result String Int
“What the hell is a Result?”, good question! You remember when you said “Well we just use toInt on the string and we’re good” and I laughed at you mockingly because I’m a dick? Think about it a bit.
- When we convert a number to a string, it is a straightforward operation. If we have
5
then it’ll become"5"
, if we have1.66
it’ll become"1.66"
, it’s just wrapping the whole thing in quotes and good to go. - But when you convert a string to a number, it isn’t as easy as that is it? Sure if your string is
"5"
then we have5
but what if your string is"foobar"
or"1,97.6"
or"three"
, what happens ifString.toInt
fails to convert our input to a number?
This is where Result
s come into play. A Result
is, surprisingly enough, the result of something – more precisely the result of something than can fail. If you remember, a while back I said Elm is all about predictability and taking into account edge-cases, and this is one of them (and a big one at that).
To force you to handle edge-cases, Elm introduced the Result
type, let’s look at its definition:
{-| A `Result` is either `Ok` meaning the computation succeeded, or it is an
`Err` meaning that there was some failure.
-}
type Result error value
= Ok value
| Err error
If you take a look at the Message
we defined it looks fairly similar, because it is also a type union, which means the same rules apply than for our messages: we can create a Result
by either doing Ok theResult
or Err theError
but not Result "error" 2
. Which makes sense, something can’t have both failed and succeeded, either the result is Ok
(literally) or it’s an Err
(usually a string with the actual error).
If you boot up the Elm REPL you can test this out very easily by trying to convert random strings to numbers:
> String.toInt "5"
Ok 5 : Result.Result String Int
> String.toInt "foobar"
Err "could not convert string 'foobar' to an Int" : Result.Result String Int
As you can see, each function that returns a Result
also specifies the type of both possibilities. So when you see Result String Int
what this means is “I’m going to return a result, either a String
if it failed, or an Int
if it worked, ok buddy?”.
This is fine and all but, even an Ok
Result is still a Result, it’s still not an actual Int
, how do we “unwrap” it to get our actual value? Well there are two ways. Just like for our Message
, since Result
is also a type union we can write a switch on it:
case (String.toInt model.incrementBy) of
Ok number -> do something with the actual number
Err error -> do something with the error
But for simple operations like converting a string to a number, more often than not, if there is an error a) you don’t care b) you can just use 0 as default. So that’s exactly what we’ll do!
The Result
module also comes with a few helpers to deal with, well, results. One of those helpers is Result.withDefault default result
, it accepts as first argument a default, and as second a Result
. If the result failed it uses the default, if it worked it uses the actual value, yay! Let’s use it in our application by using 0
as default:
numberInput = Result.withDefault 0 (String.toInt model.incrementBy)
Laying some pipe
Now, this will compile but there’s one pretty big issue with it, can you spot it? No? I’ll help: it’s horrendously unreadable because you need to read it inside out, ie. the first thing you read in the line (if you read left to right usually) is actually the last thing that you need to read in order to understand the code.
Luckily, as I mentioned in the introduction, Elm has a lot of function composition/application operators: operators that you can use to compose or apply functions (or chains of functions). Imagine how you would explain this line in actual English? Would you say:
We have a default of 0 if what we’re going to do fails, cause we’re going to convert a string to an int, and that string is our input value
Or would you say:
We have an input value, we convert it to string, and use 0 as default if it fails
Currently we have the first one, and while it’s correct, it’s a pretty dense way to explain the code. The second one makes more sense and flows more easily in your mind because it goes from input to output. Well good news, we can write our code exactly like that, using the |>
operator.
The |>
operator takes whatever is on its left, and passes it as the last argument to whatever is on its right. Dumbed-down example:
-- Before
String.toInt incrementBy
-- After
incrementBy |> String.toInt
This doesn’t look like much of an improvement, but it really starts to shine when you start having a whole chain of those just like in our case. So instead of our previous code, let’s rewrite it using |>
:
-- Before
numberInput = Result.withDefault 0 (String.toInt model.incrementBy)
-- After
numberInput = model.incrementBy |> String.toInt |> Result.withDefault 0
That sure flows more easily right? If you’re having trouble grasping this code, a personal trick that I have is to mentally insert where the value would go:
numberInput = model.incrementBy |> String.toInt (value) |> Result.withDefault 0 (value)
And if you get a lot more of those and they start being too much for one line, you can just place each on a separate line:
numberInput =
model.incrementBy
|> String.toInt
|> Result.withDefault 0
With that in mind, and since we don’t need nearly as many stuff on our page now that we can set a custom value, we can reduce our app to this:
src/Main.elm
import Html exposing (..)
import Html.Attributes exposing (class, type_)
import Html.Events exposing (onClick, onInput)
-- Model
type alias Model = {counter : Int, incrementBy : String}
model : Model
model = {counter = 0, incrementBy = ""}
-- Update
type Message = IncrementBy Int | SetIncrementBy String
update : Message -> Model -> Model
update message model =
case message of
IncrementBy howMuch -> {model | counter = model.counter + howMuch}
SetIncrementBy value -> {model | incrementBy = value}
-- View
view : Model -> Html Message
view model =
let
numberInput = model.incrementBy
|> String.toInt
|> Result.withDefault 0
in
div [] [
counterButton (IncrementBy numberInput) "Increment By",
input [type_ "number", onInput SetIncrementBy] [],
text (toString model.counter)
]
counterButton : Message -> String -> Html Message
counterButton message label =
button [onClick message] [text label]
-- Main
main = beginnerProgram {model = model, update = update, view = view}
Getting some closure
This is starting to look like an acceptable app for a throwaway dummy piece of code but we’re doing quite an unecessary step here: we’re storing the “increment by” as a string even though it will never be needed as such.
Wouldn’t it be nice if we could convert it to an int before sending it to our model? If we were in Javascript land, one way this could be accomplished would be to use a closure in our input’s event handler:
// Before
// After
setIncrementBy(Number(value))} />
We can do the same in Elm through \
and ->
which you can use in the same way you’d use the fat arrow =>
in Javascript. This means that to rewrite our code in the same way as the JS example above, we would do the following:
-- Before
input [type_ "number", onInput SetIncrementBy] [],
-- After
input [
type_ "number",
onInput (\value -> value |> String.toInt |> Result.withDefault 0 |> SetIncrementBy)
] [],
The \
denotes the start of an inline function – historically because (\
looks like a lambda sign – and the ->
denotes its body. Both are needed in this case.
Note that you can of course prepare it the same way you’d do everything:
let
handleOnInput = \value -> value
|> String.toInt
|> Result.withDefault 0
|> SetIncrementBy
in
-- [...]
input [type_ "number", onInput handleOnInput] []
-- [...]
The difference with doing it like this versus doing handleOnInput value = value
being to your discretion, as both behave exactly the same (contrarily to Javascript).
Conclusion
We’ve gone in quite different directions in this beginner introduction, and hopefully by now you have a clearer idea of how an Elm application works under the hood, and how you’d reason to build one. Over the course of this article of course we’ve stayed in the comforting warmth of the beginnerProgram
but there are other, more powerful variations of this that include support for things such as routing, or subscriptions. We also haven’t delved into package management, testing, JS interoperability (ports), requests and all that. But the goal was more to give you an idea of how an Elm app is made, and what is done differently than in your standard Javascript app.
I’ve been very enthusiastic about this language so far but of course it wasn’t thought out to be applied to every use case you might have. It is very opiniated and intentionally restricts how you’d work in order to guide to one true accepted™ way it encourages.
The Good Stuff
Elm is a statically typed language with a very precise and human friendly compiler. It’s a nice fit for SPAs and rapid iterations: because its type system is robust, you can refactor at east without fear of breaking anything. You can’t mess up an Elm codebase because it simply wouldn’t let you. Since it also comes with so many things built-in, it’s also quite fast to get started with since even without using packages you get the benefits of a Redux-like architecture and workflow.
Even to learn, Elm can be more approachable than other languages because while most of what it offers is of certain complexity, it doesn’t nearly have the baggage that languages like Javascript are dragging around. Everything was planned as a cohesive whole, as such it’s easier to reason with the language itself and to locate/use things because you know everything available was put for a reason.
The Maybe Not As Good Stuff
That being said, Elm remains a small language, developped by one person with a clear vision. It doesn’t have Facebook-backing, nor weekly roadmaps or anything. A new stable version of the ecosystem comes out when it comes out and when the creator deems there are enough changes that warrant it. There can be extended periods of time between releases.
As a result the community is fairly small, and while you will find typings for a lot of popular libraries, you will still need to write them yourselves if there aren’t any, which can be complicated for a first time. The same can be said for resources in general: documentation, articles, etc.
Generally speaking the faults you’d want to input to Elm are at the implementation level. Design-wise, Elm does precisely what it was made for and with remarkable efficiency. But you will still hit regular road-blocks which learning it because of how alien some of its paradigms are when coming from Javascript. There will be obscure type errors, you will pull out some hair properly handling dates and anything unpure by design, or with your usual JS libraries.
There is also some things still missing in the language/framework as a whole, like server-side rendering and such which might be a definite block for some.
The Subjective Median Opinion of the Stuff
I know these things because I’ve struggled with them personally, yet here I am because despite any of the flaws and growing pains that come with working with Elm, it remains a fantastic experience.
To have such a small little box that can do so much and with such safety is like having a Raspberry Pi for your applications. You’re probably not gonna do everything with it, but damn if it isn’t fun to tinker with it and create things rapidly, knowing there is nothing of value you can possibly mess up.
If this has you stuck between two chairs know that there are other languages out there that share Elm’s philosophy such as Reason and ReasonReact. So if you like the idea but not the implementation then I do recommend to check out these alteratives. There will be another article on Reason itself because I believe it’s a very valid choice when looking for Elm alternatives that have more backing at the moment.
Would you like to know more?
If I’ve peaked your interest here are a couple of good resources to get properly started with:
- Official website of course, since it contains some examples and a book to get started with
- Elm Programming, another more approachable (and complete) online book with lots of very good information
- Official subreddit, also a good active place to check out with lots of people ready to help and answers to lots of questions newcomers may have
Member discussion