Perfection is finally attained not when there is no longer anything to add, but when there is no longer anything to take away. — Antoine de Saint-Exupery
Back in 2006, front-end development was simple and imperfect. We were really excited about building apps on web browsers. Amazing stuff like EditGrid and Netvibes were built. It's been 12 years since then, the way we build web apps has changed quite significantly because we've been hoping that incremental improvements will fix fundemental issues with JavaScript, HTML and CSS.
And incremental improvements made front-end development quite messy. Developers waste hours bruteforcing tools and libraries until they can finally work together. We have excellent developer tools, but error messages are mysterious. The community can never agree on minor style issues. There is lots of ways to solve problems, but none is simple, scalable and productive at same time.
It's time to start benefiting from the advances in programming language design.
It was 2012 when a Harvard student named Evan Czaplicki designed a new, functional reactive programming language called Elm as his thesis project. He also implemented this language and shared with rest of the world. Elm gained some popularity since then, while inspiring JavaScript community to build libraries such as Redux.
Elm syntax doesn't look like JavaScript, so you might be afraid of learning curve. In fact, Elm is smaller and simpler than JavaScript, and it shouldn't take longer than 8 hours to go through all this documentation, including doing some exercises. I belive it's a good investment, plus it's fun.
1. What is Elm?
Elm is a statically typed functional programming language specifically designed for building web apps.
It's really fast, and got an excellent compiler that makes it possible to have no runtime errors.
There is no undefined
or null
. Its core language is minimalistic and simple that you can walk through in half an hour.
Example Code
Before exploring Elm more in the further sections, here is a piece of Elm and JavaScript/HTML code side by side.
Both code do the same job; we define a function named double
, it returns the double of given argument n
.
We print result of 5 * 2
to the screen.
module Double exposing (main)
import Html exposing (text)
double : Int -> Int
double n = n * 2
main =
text (toString (double 5))
<html>
<body>
<script type="text/javascript">
function double (n) {
return n * 2
}
document.body.innerHTML = double(5)
</script>
</body>
</html>
Why is it different?
Elm is not an evolution of JavaScript; it's a holistical alternative for the whole JavaScript ecosystem. Instead of being an incremental improvement, Elm designs a GUI development experience that is simple, straightforward and robust.
While type annotation is optional, Elm can infer all the types and its compiler catches errors in the compile time by providing you very specific and human friendly compile errors. In the other words, errors happen in the compile time in front of the developer, instead of runtime, in front of users.
Here is an example error message when we misspell double
as doule
:
$ elm-make double.elm
-- NAMING ERROR ----------------------------------------------------- double.elm
Cannot find variable `doule`
9| text (toString (doule 5))
^^^^^
Maybe you want one of the following?
double
Detected errors in 1 module.
Tools
Elm provides a standard toolset that gets versioned and shipped together. Below is an overview of standard tools and a few extra tools that are de-facto standars in the community;
Tool | Desc | Core | Community |
---|---|---|---|
elm-make | Compiler | ✓ | |
elm-package | Package manager. Example Package | ✓ | |
elm-format | Code formatter. No config files, no minor style issues. | ✓ | |
elm-reactor | A local server auto-compiles elm files. | ✓ | |
elm-test | Testing library | ✓ | |
elm-html | HTML rendering library backed by virtual-dom. | ✓ | |
elm-css | CSS library and compiler | ✓ |
Install
Before we start coding, let's make sure you've got Elm installed in your system;
- Mac Installer
- Windows Installer
- Other Platforms:
yarn global add elm
ornpm install -g elm
After installation, create a folder named elm-sandbox
in your home folder and open up elm-repl
to run following code:
> String.reverse "I love tea!"
Works? Perfect. elm-repl is useful for experimenting the language basics. Use it for all short examples in the language reference.
Now open up another terminal window and cd
into the elm-sandbox
folder you've created. This time, run elm-reactor
command.
Once elm-reactor
is running, open your personal code editor and save following code into the sandbox as hello-world.elm
:
import Html exposing (text)
main =
text "Hello world"
Now open up localhost:8000/hello-world.elm
. Do you see the "Hello world" ? If yes, you're ready to rock. If not, please make sure you've
followed the steps correctly.
2. The Language
In this section we'll explore Elm language. Make sure you've completed the installation steps explained above.
Functions
Here is how we define a
simple function called multiply
; it takes two arguments and returns multiplication of them:
> multiply x y = x * y
There is no return
statement because each expression will result in a value. Let's call the function we've just defined and see the result:
> multiply 2 5
-- 10 : number
Partial Application
You can create partial functions easily. Let's define a function that multiplies given number by 2, by using the multiply
function we defined above:
> multiplyBy2 = multiply 2
Elm creates a partial function if all arguments are not passed. This makes Partial Application really easy:
> multiplyBy2 5
-- 10 : number
Pipelines
Multistep data operations can be done using pipelines. Here is comparision of same code with pipelines and without:
-- With pipelines
reverseUppercase text =
text
|> String.toUpper
|> String.reverse
-- Without pipelines
reverseUppercase text =
(String.reverse (String.toUpper text))
They'll both return MLE
when we call them like reverseUppercase "Elm"
. We can define reverseUppercase
using function composition, too. Check it out below.
Function Composition
Function compositions are useful when we want to create a new function from other functions.
In Elm, it can be done using <<
(right to left) and >>
(left to right) operators. While pipelines require values and return values, function composition only require functions and return functions as a result.
Here is the comparision of same code with function composition and without:
-- With function composition
> reverseUppercase = String.toUpper >> String.reverse
-- Without function composition
reverseUppercase text =
(String.reverse (String.toUpper text))
Above code creates a new function that takes a parameter, passes it to toUpper
first. Then the result of toUpper
gets passed to reverse
and
whatever reverse
returns is the result of the function we've defined.
Let
let
expression allows us define variables inside functions. Here is an example:
someRandomMath x =
let
y = 5
z = 10
in
(x + y) * z
Let's call the function we've just created:
someRandomMath 4
-- 90 : Number
If Expressions
It probably looks quite familar to you;
above100 n =
if n > 100 then
"Yeah, it is above 100"
else
"Nope, it's not"
The function we've defined takes an argument, checks if it's bigger than 100 and returns a string. No return statement needed, the function naturally results in a value.
Lists
Lists hold collection of same type of values. You can define them with brackets and use the functions in List package:
> numbers = [ 3, 1, 2 ]
> List.sort numbers
-- [1, 2, 3] : List number
You can manipulate list elements using the map
method. The result will be a new list:
> double n = n * 2
> List.map double numbers
-- [6, 2, 4]
Tuples
Tuples hold fixed number of values; but they can be any type.
> (True, "Hey there")
This is very helpful for returning multiple values from a function. The most common use case
you'll see is the update
functions every Elm program has. They return a new model and a command
every time an update happens in the program;
update : Msg -> Model -> (Model, Cmd Msg) -- It takes Msg and Model, returns Model and Cmd Msg
update msg model =
PlayMusic ->
( { model | playing = True }, Cmd.none )
Records
Records are equivalent of JavaScript objects in Elm. Here is how we can define a simple record:
> kanye = { name = "Kanye West", children = 2 }
We all know Kanye and Kim had a new child recently and we need to update that record. Easy-cheesy:
> newKanye = { kanye | children = 3 }
Two things to notice in the above code:
|
allows us creating a new object by updating some fields of another.- We can't make recursive assignments.
kanye = { kanye | children = 3}
would not compile.
You can access value of a record field with .
followed by field name:
> .name kanye
-- Kanye West : String
> .children kanye
-- 2 : Number
As you may guess, .children
above is a function even if we didn't define. So we can use it with other functions that works with functions, List.map
for example:
> lilwayne = { name = "Lil Wayne", children = 0 }
> List.map .children [kanye, lilwayne]
[3,0] : List number
Elm supports destructuring, too. Here is how we can benefit from destructuring;
hasChildren rapper =
let
{ children } = rapper
in
children > 0
You can define a function that destructures given parameter:
> hasChildren { children } = children > 0
> hasChildren newKanye
-- True : Bool
These are useful, we might need to define an alias for this record type though. Let's check how types work in Elm;
Types
Type Aliases
Type aliases give alternate name for an existing type. For example;
type alias Text = String
Now Text
is an alternate name for String. A very common use of type aliases is to give record types a name:
type alias Rapper =
{ name: String
, children: Int
}
The Rapper
type alias we've defined is just an alias for { name: String, children: Int }
. It's
obviously more convenient to create aliases for record types that we use in multiple places in the codebase.
Type aliases are constructors at same time;
drake = Rapper "Drake" 0
-- { name = "Drake", children = 0 } : Rapper
Let's define hasChildren
again, using type annotation this time:
hasChildren : Rapper -> Bool
hasChildren rapper = rapper.children > 0
Union Types
A union type specifies exactly what values it can have. For example;
type AudioEvent
= Play
| Pause
| VolumeChange Int
We've just defined a new type named AudioEvent
and it can only have specified three values.
Values might have their attachment, too. For example,VolumeChanged
expects an Int
value to be passed along.
Want to see these values being used in a practical example? See the next section; case-of.
case-of
Union types and case-of
constructs are the most powerful features of Elm. They let us define a complex
conditions and handle them naturally.
Here is how we can use AudioEvent
type we've defined in the previous section:
onAudioEvent : AudioEvent -> String
onAudioEvent event =
case event of
Play ->
"Starting music..."
Pause ->
"Pausing music..."
Volume n ->
"Changing volume to " ++ (toString n)
We'll benefit from union types and case-of in the most critical parts of our Elm applications.
3. Let's Build Apps!
Every Elm program consists of model, view and an update function that handles all the updates to the model. What does this mean ? Let's build a few examples to understand it.
Radio Player
We want to build a little radio player. It will have two buttons: Play and Pause. Whenever user clicks play, we'll start playing Radio Paradise.
Every Elm program starts by defining types. We'll use type alias for defining the model, and union type for defining the messages:
type alias Model =
{ playing : Bool
, src : String
}
type Msg
= Play
| Pause
Now we have an idea about what this program does. Model
describes its data model, and Msg
describes
the external events that our program should react to. The next step is to create the update
function, which
implements the changes we've defined.
update : Msg -> Model -> Model -- Update function takes `Msg` and `Model` as parameters, and returns a new `Model`.
update msg model =
case msg of -- What type of Msg we received ? Play or Pause ?
Play ->
{ model | playing = True } -- If the message is play, set the playing field as True.
Pause ->
{ model | playing = False } -- See `Records` section if this syntax looks weird to your eyes.
We defined the types, implemented the update function that handles the Msg
variations that could be sent from
an external source. Time to build the user interface.
Every Elm program needs a view
function that takes Model
and returns Html
.
We use the core Html package to create the view.
Please run elm-package install elm-lang/html
command in your project folder to get this package installed.
Here is a simple example of creating HTML elements:
import Html exposing (h1, text)
import Html.Attributes exposing (class)
main =
h1 [class "title"] -- Attribute list
[text "Hello World"] -- Children elements list
<html>
<body>
<h1 class="title">
Hello World
</h1>
</body>
</html>
The actual view
function we'll code is little bit more complex than the above example; it should return two different interfaces;
play button when the music is stopped, pause button and audio elements when the music is playing.
We'll have the condition in the main view function, and create two more functions to implement the views:
view : Model -> Html Msg
view model =
if model.playing then
playingView model
else
notPlayingView model
notPlayingView : Model -> Html Msg
notPlayingView model =
button [ onClick Play ] [ text "Play" ]
playingView : Model -> Html Msg
playingView model =
div []
[ button [ onClick Pause ] [ text "Pause" ]
, audio [ src model.src, autoplay True, controls True ] []
]
Our program is almost ready to run, it's missing the main
function every Elm program needs;
main =
Html.beginnerProgram
{ view = view
, model = Model False "http://stream-tx4.radioparadise.com/mp3-192"
, update = update
}
Elm reads the whole file before executing, so you can refer to variables defined later. It's a common practice put the main
on top of your file.
Once you save your code, compare what you've got with full code of this example.
Run / Compile
Ready to see your code working? Run elm-reactor
in the project directory and open localhost:8000
. You can make changes
and refresh your browser to see the changes.
Once you finalize the changes, you can either compile HTML or JavaScript using elm-make
. I personally prefer creating my own HTML and
injecting compiled Elm code in:
$ elm-make radio.elm --output radio.js
The compiled JavaScript file can be injected to anywhere in the DOM:
<html>
<body>
<div id="elm"></div>
<script type="text/javascript" src="radio.js"></script>
<script type="text/javascript">
Elm.App.embed(document.querySelector("#elm"))
// P.S `Elm.App` path can be different depending on the package name.
</script>
</body>
</html>
We got a simple app working already, wow! Now I assume you want to make your application look pretty. We'll learn how to style our Elm programs with CSS.
CSS
elm-css allows us style our programs easily. We can define all the CSS properties inline in the view functions,
they'll get compiled and added into the DOM tree inside style
elements automatically.
Here is a simple example and its output:
module Main exposing (..)
import Css exposing (..)
import Html.Styled exposing (h1, text)
import Html.Styled.Attributes exposing (css)
main =
Html.Styled.toUnstyled title
title =
h1
[ css
[ backgroundColor (rgb 255 200 50)
, padding (px 20)
, textAlign center
, fontSize (em 3)
]
]
[ text "Hello World" ]
<!DOCTYPE html>
<html>
<body>
<h1 class="_57c3f2a9">
<style>
._57c3f2a9 {
background-color: rgb(255, 200, 50);
padding: 20px;
text-align: center;
font-size: 3em;
}
</style>
Hello World
</h1>
</body>
</html>
There is a few important lines we need to pay extra attention in the above example:
- Line 4, 5: Instead of
Html
, now we import DOM elements fromHtml.Styled
- Line 9: Elm still wants us to return
Html
, so we convertHtml.Styled
intoHtml
. - Line 14: We define CSS properties as a list inside the attributes set.
HTTP Requests & JSON Parsing
We all make API requests and they're often JSON. In this imaginary Elm program, we'll pull list of songs from a radio API.
First of all, we need to define the kind of response we expect:
type alias APIResponse =
{ songs : List String }
Secondly, we need to create (or append) a Msg
value for the HTTP event.
type Msg = APILoaded (Result Http.Error APIResponse)
Now we're ready to define how we'll make the request and parse the response.
import Json.Decode as Decode
import Http
sendRequest : Cmd Msg
sendRequest =
Http.send APILoaded (Http.get "/api/songs" responseDecoder)
responseDecoder : Decode.Decoder APIResponse
responseDecoder =
Decode.map APIResponse
(Decode.field "songs" (Decode.list Decode.string))
{
"songs": [
"Porcupine Tree — I Drive The Hearse",
"Tom Waits — Hold On",
"Florence + The Machine — Leave My Body",
"The Eagles — Seven Bridges Road"
]
}
The above code makes a GET request to '/api/songs' path, and raises APILoaded
event passing the parsed response returned from responseDecoder
.
Now we can handle APILoaded
message in the update
function:
update msg model =
case msg of
APILoaded (Ok response) ->
{ model | songs = response.songs }
APILoaded (Err err) ->
{ model | error = (toString err) }
Init
init
functions define the initial model and the commands that should be executed immediately. Imagine a case you need to make an API request
to pull the content of a page, init
function is where you make that initial request;
init : ( Model, Cmd Msg )
init =
( Model [], sendRequest) -- See HTTP chapter above for definition of sendRequest
You can execute multiple commands using Cmd.batch
;
init : ( Model, Cmd Msg )
init =
( Model [], Cmd.batch (List.map sendRequest ["foo", "bar"]))
If you prefer not to execute any commond, use Cmd.none
:
init : ( Model, Cmd Msg )
init =
( Model [], Cmd.none)
In the earlier radio player example we've used Elm.beginnerProgram
. In rest of the examples we'll use Elm.program
instead.
It requires us to provide subscriptions
which we'll cover after init
.
Here is an example main
function using Elm.program
:
main : Program Never Model Msg
main =
Html.program
{ init = init
, view = view
, update = update
, subscriptions = always Sub.none
}
Subscriptions
Subscriptions allow Elm programs to listen external input such as browser events, timers, port messages.
In the following example, we'll listen for keyboard events. If user presses space key (KeyCode 32),
we'll make a change in the model. (P.S elm-lang/keyboard
package is required.)
import Keyboard
type Msg = Keypress Keyboard.KeyCode
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Keypress code ->
if code == 32 then
( { model | muted = True }, Cmd.none )
else
( model, Cmd.none )
subscriptions : Model -> Sub Msg
subscriptions model =
Keyboard.downs Keypress
Ports
We can define ports for both sending and receiving messages between Elm programs and external JavaScript code. Imagine a case our program needs to interact with a web worker written in JavaScript. We need to define a port for sending messages from Elm to JavaScript, and another port for receiving message from JavaScript;
Following example defines the type of what we'll receive, creates the ports and subscribes to the incoming port.
port module MyProgram -- Modules with ports has to declare it.
type Msg = NewMessageToElm String
port toJS : String -> Cmd msg
port toElm : (String -> msg) -> Sub msg
subscriptions : Model -> Sub Msg
subscriptions model =
toElm NewMessageToElm
update : Model -> Msg -> (Model, Cmd Msg)
update msg model =
Case msg of
NewMessageToElm str
({ model | messageFromJS = str }, Cmd.none)
<html>
<head></head>
<body>
<script type="text/javascript" src="myprogram.js"></script>
<script type="text/javascript">
const myProgram = Elm.MyProgram.fullscreen()
myProgram.ports.toJS.subscribe(function (msg) {
console.log('[elm-to-js]', msg)
})
myProgram.ports.toElm.send("Hey there")
</script>
</body>
</html>
See the following section, Example 2: Fake chat
for a working example using ports.
Example 2: Fake Chat
This time we'll create a fake chat app to exercise what we've learnt in last sections. Our program will;
- List messages (
model.messages
) - Receive new messages from user (
model.input
) - Send every new message from Elm to JS (
Msg / SendToJS
viaport toJS
) - Modify messages in JS and send back to Elm (
Msg / NewMessage
viaport toElm
) - Add messages received from JS to (
model.messages
)
As always, we start coding types first:
port module FakeChat exposing (..)
type alias Model =
{ input : String
, messages : List String
}
type Msg
= InputChange String
| SendToJS
| NewMessage String
We need two ports, incoming (toElm) and outgoing (toJS).
port toJS : String -> Cmd msg
port toElm : (String -> msg) -> Sub msg
subscriptions : Model -> Sub Msg
subscriptions model =
toElm NewMessage
In the subscriptions
functions above our program started listening messages from the toElm
port.
When JS program sends a message, our updater will receive NewMessage String
.
update
function is the key part of this program as usual. In the below code we'll handle
SendToJS
messages which gets sent when user presses the submit button. In return, we'll
send a command created by toJS
port. This is all we need to send messages to JavaScript.
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
InputChange newInput ->
( { model | input = newInput }, Cmd.none )
SendToJS ->
({ model | input = "" }, toJS model.input)
NewMessage incoming ->
({ model | messages = model.messages ++ [incoming] }, Cmd.none)
Now we need to get the views. Our Elm program will be a minimalistic interface with list of messages
and an input. Every time the value of input changes, it'll fire InputChange
message and our updater
we've defined above will set model.input
to the new input value.
view : Model -> Html Msg
view model =
div []
[ viewMessages model.messages
, input [ type_ "text"
, placeholder "Type a message"
, onInput InputChange
, value model.input
] []
, button [ onClick SendToJS ] [ text "Send to JS" ]
]
viewMessages : List String -> Html Msg
viewMessages messages =
ul []
(List.map viewMessage messages)
viewMessage : String -> Html Msg
viewMessage message =
li []
[ text message ]
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<script type="text/javascript" src="ports.js"></script>
<script type="text/javascript">
// `fullscreen` is a shorthand function for inserting Elm interface into the DOM
const fakeChat = Elm.FakeChat.fullscreen()
const names = ["Alice", "Bob", "John"]
// Start listening messages sent from elm
fakeChat.ports.toJS.subscribe(function (msg) {
console.log('[elm-to-js]', msg)
const name = names[Math.floor(Math.random() * names.length)]
// Append a random nickname in front of the message, send it back to Elm
fakeChat.ports.toElm.send(`<${name}> ${msg}`)
})
</script>
</body>
</html>
Compile this program with elm-make
and include it in an HTML page as you see in the above example.
Compare your code with full working example
if you face any unexpected problems.
Codebase Structure
This part of the guide is possibly misleading beginners. Elm is a functional language and it's recommended to not create components that contains state. Checkout sortable table example for the recommended approach on components.
While we can create quick prototypes in one-file Elm programs, we'll want to modularize the codebase into smaller parts in the real world. A good practice is to split your app into folders, then split every folder into different files (Types, State, View etc...).
Recently I built a radio player app (you can actually try it). It consists of four isolated components:
- History: List of recently played songs. Makes API requests.
- Player: Plays audio.
- Wallpaper: Shows a photo in the background.
- Container: Puts all three components in together.
Each component consists of three common modules;
- State.elm:
init
,update
andsubscriptions
- Types.elm: all the type declarations. It shouldn't import any sibling modules.
- View.elm: view functions.
Some components have more modules depending on what they do. The final directory layout of the radio player I mentioned is following:
── elm-package.json
── src
├── main.elm
├── Container
| ├── State.elm
| ├── Types.elm
| ├── View.elm
├── History
| ├── Rest.elm
| ├── State.elm
| ├── Types.elm
| ├── View.elm
| ├── Style.elm
├── Player
| ├── Events.elm
| ├── Icons.elm
| ├── State.elm
| ├── Types.elm
| ├── View.elm
| ├── Style.elm
├── Wallpaper
├── State.elm
├── Types.elm
├── View.elm
Container Components
Container components not only put views together, they also call init
functions of every component, distribute
messages correct component's update
function, and get subscriptions
started.
We keep our application state unified, so our Model
consists of the child models:
module Container.Types exposing (..)
import History.Types
import Player.Types
import Wallpaper.Types
type alias Model =
{ history : History.Types.Model
, player : Player.Types.Model
, wallpaper : Wallpaper.Types.Model
}
type Msg
= HistoryMsg History.Types.Msg
| PlayerMsg Player.Types.Msg
Notice that Msg
is also a parent type that categorizes the child messages. Container init
will
need to call every child init
function, update its state from the results and call the
commands they've returned using Cmd.batch
:
module Container.State exposing (init, update, subscriptions)
import Container.Types exposing (..)
import History.State
import Player.State
import Wallpaper.State
init : ( Model, Cmd Msg )
init =
let
( history, historyCmd ) =
History.State.init
( player, playerCmd ) =
Player.State.init
wallpaper =
Wallpaper.State.init
in
( Model history player wallpaper
, Cmd.batch
[ Cmd.map HistoryMsg historyCmd
, Cmd.map PlayerMsg playerCmd
]
)
You might have noticed how we map child commands to Container Msg
values. This is very important, because it allows
us to categorize the messages in the Container update
:
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
HistoryMsg fMsg ->
let
( history, historyCmd ) =
History.State.update fMsg model.history
in
( { model | history = history }, Cmd.map HistoryMsg historyCmd )
PlayerMsg fMsg ->
let
( player, playerCmd ) =
Player.State.update fMsg model.player
in
( { model | player = player }, Cmd.map PlayerMsg playerCmd )
Subscriptions needs to be mapped called as a batch;
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.batch
[ Sub.map HistoryMsg (History.State.subscriptions model.history)
, Sub.map PlayerMsg (Player.State.subscriptions model.player)
]
Finally we're ready to put the views together. As we categorized the model and the messages properly, it'll be straightforward:
view : Model -> Html Msg
view model =
div []
[ History.View.view model.history
|> Html.map HistoryMsg
, Player.View.view model.player
|> Html.map PlayerMsg
, Wallpaper.View.view model.wallpaper
]
4. Wrap Up
Hopefully this was useful for you. Open up a pull request for any improvements, corrections. You can also drop me an e-mail for sharing any thoughts or asking questions.
Here are some other reasources that I recommend:
Community channels:
- Slack
- #elm on Freenode
- Mailing List
Other: