This projects aims to introduce Elm basics as a hands-on project
Install Git if it is not already done
Prerequisites: Homebrew
- Install elm:
brew install elm - Install or update npm:
brew install npmorbrew upgrade npm - Install elm-live:
npm install -g elm-live
- Install elm:
apt-get install elm - Install or update npm:
apt-get install npmorapt-get upgrade npm - Install elm-live:
npm install -g elm-live
- Install elm: Windows Installer
- Install or update npm: Node
- Install elm-live:
npm install -g elm-live
Install elm-test: npm install -g elm-test
Install elm-format following this startup guide: Install Elm format
This aims to format using Elm style guide when saving.
➜ ~ elm --version
0.19.0
➜ ~ elm-live --version
3.2.3
➜ ~ elm-test --version
0.19.0-rev6
➜ ~ elm-format --help
elm-format 0.8.1
Usage: elm-format [INPUT] [--output FILE] [--yes] [--validate] [--stdin]
[--elm-version VERSION] [--upgrade]
Format Elm source files.
Available options:
-h,--help Show this help text
--output FILE Write output to FILE instead of overwriting the given
source file.
--yes Reply 'yes' to all automated prompts.
--validate Check if files are formatted without changing them.
--stdin Read from stdin, output to stdout.
--elm-version VERSION The Elm version of the source files being formatted.
Valid values: 0.18, 0.19. Default: auto
--upgrade Upgrade older Elm files to Elm 0.19 syntax
Examples:
elm-format Main.elm # formats Main.elm
elm-format Main.elm --output Main2.elm # formats Main.elm as Main2.elm
elm-format src/ # format all *.elm files in the src directory
Full guide to using elm-format at <https://github.com/avh4/elm-format>
➜ ~
- Checkout the project and switch to branch
step-0:
git checkout step-0
- Try compiling the code:
elm make src/Main.elm
When running this command, elm transpiles elm code to javascript inside index.html file.
-
Open
index.htmlin a browser and you should see your first Elm application -
Rewrite
index.htmlfile with following content:
<!DOCTYPE HTML>
<html>
<head>
<meta charset='UTF-8'>
<title>Main</title>
</head>
<body>
<div id='elm-f0111bc4e658d0f98db96260c16f7e49'></div>
<script src='main.js'></script>
<script>
Elm.Main.init({ node: document.getElementById('elm-f0111bc4e658d0f98db96260c16f7e49') });
</script>
</body>
</html>-
Run the following command:
elm make src/Main.elm --output=main.js -
Reload
index.htmlin your browser. You should see the same output as before but now, the elm compiler will generatemain.js.
elm-live is a flexible dev server for Elm which includes hot-reload.
-
Install
elm-liveif not already done -
Run the following command:
elm-live src/Main.elm --port=1234 --open -- --output=main.js
- Update
Main.elmto change the output message and save.
See your browser and notice it should have reloaded the page automatically.
This REPL (Read Eval Print Loop) allows you to run any elm statement in the terminal (eg. to check syntax or semantics).
-
Run elm repl:
elm repl -
Develop and evaluate following expressions:
- Adding
40and2(let's start easy)
40 + 2
- Create a function which adds two numbers (integer) and call it
> add i j = i + j <function> : number -> number -> number > add 40 2 42 : number
- Create a function which takes a login in parameter and returns "Hello " + login
> hello login = "Hello " ++ login <function> : String -> String > hello "Rémi" "Hello Rémi" : String
- Create a function which takes a
Maybe Stringand returns "Hello " + login if login is defined and "Hello World" otherwise.
> hello maybeLogin = case maybeLogin of \ | Nothing -> "Hello World"\ | Just login -> "Hello " ++ login <function> : Maybe String -> String > hello (Just "Rémi") "Hello Rémi" : String > hello Nothing "Hello World" : String
- Adding
Bonus for currying:
> multiply a b = a * b
<function> : number -> number -> number
> multiplyByThree a = multiply a 3
<function> : number -> number
> multiplyByThree 10
30 : numberAs for node, elm has a package manager which let us publish or download packages from Elm packages.
A package is one or several modules and you can see them as a collection of utilities function (that are exposed).
Package are prefixed with the module author nickname (or elm for core modules) and then the package name separated with a / (eg. mdgriffith/elm-ui).
-
Install
elm/svgpackage -
Notice the dependency package in the
elm-stuff/packagesdirectory -
Use your application to draw a circle (use documentation) or any forms you want
Online editor: Ellie
Use this online editor if you want to try some code without having a local project.
True or False. Nothing else to explain.
Kind of easy so let's skip this one (+, -, * and /)...
Small tips: // is also a division operator but it returns the Int value of it.
Note that these operators are functions so they can also be used in prefix style like (+) 40 2
Using it like 40 + 2 is called the infix style.
Nothing specific here except the not equal operator which look more like a different: /=
(==) = eq
(/=) = neq
(<) = lt
(>) = gt
(<=) = le
(>=) = ge
Running 40 + 2 * 100 will return 240 (operator precedence).
If we want to execute 40 + 2 before multiplying it with 100 we need to explicitely tell so:
(40 + 2) * 100
message : Int -> String
message age =
if age > 30 then
"You are older than me"
else
"You are younger than me"message : Int -> String
message age =
case age > 30 of
True -> "You are older than me"
False -> "You are younger than me"- Fact 1: A function always returns something!
- Fact 2: Explicitely write function signature helps!
- Fact 3: Function are testable (easily)
increment : Int -> Int -> Int
increment a b = a + bYou can apply a function partially (currying):
multiplyByTwo : Int -> Int
multiplyByTwo a = increment a aWhen calling a function like:
add : Int -> Int -> Int
add a b = a + bYou have two possibilities:
add 40 2
2 |> add 40|> means appendLeft and helps for code readability most of the time.
let statement helps you extracting temporary variables before returning the function results.
For example:
addAndMultiplyBy Int -> Int -> Int -> Int
addAndMultiplyBy a b c =
let
addResult : Int
addResult = a + b
in
addResult * cThis can also help in code-readability as to extract result operations in named variables.
Elm program can be divided into three cleanly separated parts:
- Model — the state of your application
- update — a way to update your state
- view — a way to view your state as HTML
Writing a new Elm module often starts with the following skeleton:
module Module exposing (..)
import ...
-- MODEL
type alias Model = { ... }
-- UPDATE
type Msg = Write | ...
update : Msg -> Model -> Model
update msg model =
case msg of
Write -> model
_ -> ...
-- VIEW
view : Model -> Html Msg
view model =
...You can enable Elm debugger easily by adding --debug option to your elm-live start-up:
elm-live src/Counter.elm --port=1234 --open -- --output=main.js --debug
This gives you access to the bottom right panel:

Clicking on it will give you access to the history of any action done in your application so you can replay them one by one:

This can be very useful whenever you want to debug your application behaviour.
- Create a new Elm file
Counter.elmin yoursrcdirectory as below:
module Counter exposing (Model, Msg(..), initialModel, main, update, view)
import Browser
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)
type alias Model =
{ count : Int }
initialModel : Model
initialModel =
{ count = 0 }
type Msg
= Increment
update : Msg -> Model -> Model
update msg model =
case msg of
Increment ->
{ model | count = model.count + 1 }
view : Model -> Html Msg
view model =
div []
[ button [ onClick Increment ] [ text "+1" ]
, div [] [ text <| String.fromInt model.count ]
]
main : Program () Model Msg
main =
Browser.sandbox
{ init = initialModel
, view = view
, update = update
}
-
Run the program:
elm-live src/Counter.elm --port=1234 --open -- --output=main.js -
Implement
decrementaction
- Create a new Elm module:
Input.elm - This module should expose an input text and when typing, the state should be updated and a message
Hello+ login should appear - Bonus: Write "Hello World" whenever the typed value from input is empty
If Elm is so reliable, it is because it relies on pure functions only so there will be no unexpected behaviour.
Side effects are any interactions with the outside world like:
- Send and receive data from a remote HTTP server.
- Save data to a local storage.
- Generate random numbers.
- Request a JavaScript library to perform an operation
But it can also be any operations like:
- Listen for web socket messages.
- Listen for location changes.
- Listen for clock ticks.
- Listen for an output generated by a JavaScript library
These side effects are painful when developing an application because, by definition, we can't make sure they will always be executed as expected.
Elm Runtine will act as a proxy for any outside operations so our application is protected from anything unexpected happening.
Let's take an example (from elm/http):
{-| 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 errorWhen requesting a resource via Http, Elm will allow us to deal with a Result than can either be Ok or Err if something bad happened.
In the end, when handling this result, anything will be explicitly handled as below:
type Msg = LoadedItem (Result Http.Error Item)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
LoadedItem result ->
case result of
Ok fetchedItem ->
( { model | item = fetchedItem }
, Cmd.none
)
Err error ->
model |> failure error
...In this particular case, the returned error can take multiple form as defined in the documentation:
{-| A `Request` can fail in a couple ways:
- `BadUrl` means you did not provide a valid URL.
- `Timeout` means it took too long to get a response.
- `NetworkError` means the user turned off their wifi, went in a cave, etc.
- `BadStatus` means you got a response back, but the [status code][sc]
indicates failure.
- `BadPayload` means you got a response back with a nice status code, but
the body of the response was something unexpected. The `String` in this
case is a debugging message that explains what went wrong with your JSON
decoder or whatever.
[sc]: https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
-}
type Error
= BadUrl String
| Timeout
| NetworkError
| BadStatus (Response String)
| BadPayload String (Response String)With Elm runtine, we can be sure that any side effects will be handled properly and should be solved in our application properly.
- Install
elm/randompackage - Create a new Elm module (
Dice.elm) as a Browser.element application (so we can interact with the outside world)
main : Program () Model Msg
main =
Browser.element
{ init = initialModel
, view = view
, update = update
, subscriptions = subscriptions
}- The application should be composed of a button to generate a number between 1 and 6 and the value should be rendered next to it as below:

- Few tips:
type alias Model =
{ value : Int }type Msg
= Roll
| Rolled IntTo generate a random value, you can use this function which returns a Cmd Msg (Roll):
generate : Int -> Int -> Cmd Msg
generate min max =
max
|> Random.int min
|> Random.generate RolledAs Elm application are basically made of pure functions and thanks to the elm compiler it drives us to build reliable application very easily and it does help us a lot to unit test our application as well.
Indeed, it is a lot easier to test a pure function as, by definition, it should always returns the same value based on the same input.
There are few requirements before running our first test:
- install
elm-testwhich is a node package allowing us to run the elm tests:npm install -g elm-test - install Elm package
elm-explorations/testto get helpers for writing our unit tests - run
elm-test initto initialize your project with unit tests (it creates atestsdirectory)
Once the requirements are ran, you should get tests/Example.elm in which you can copy/paste the following code:
module Example exposing (myFirstTest)
import Expect exposing (Expectation)
import Test exposing (..)
myFirstTest : Test
myFirstTest =
test "1 should be equal to 1" <|
\_ ->
1
|> Expect.equal 1To run our first test, you can run: elm-test in your project directory and you should see as following:
elm-test 0.19.0-rev6
--------------------
Running 1 test. To reproduce these results, run: elm-test --fuzz 100 --seed 363451075738594 /Users/Remi/projects/perso/elm-hands-on/tests/Example.elm
TEST RUN PASSED
Duration: 195 ms
Passed: 1
Failed: 0elm-test comes with a watch mode allowing us to change our code and getting our unit tests replayed automatically.
To run it with watch mode, run elm-test --watch.
Try updating your unit test afterwards and check the result in your console.
- Create unit tests to validate
String.reversefunction withkayak- it should returnkayak - Create unit tests to validate
String.reversefunction withelm- it should returnmle
When talking about reversing a string there is a property coming in our head which is: the provided word can be reverse twice and should return the original word.
So, we can imagine testing this property instead of unit testing a single word as before.
We can imagine generating a random word and after calling this String.reverse function twice, asserting the result word is the same as the original one.
elm-test allows us to deals with this property base testing working with Fuzz tests.
Example:
myFirstFuzzTest : Test
myFirstFuzzTest =
Test.fuzz Fuzz.int "My first fuzz test " <|
\randomValue ->
randomValue
|> Debug.log "random value = "
|> Expect.equal randomValueFew differences with the unit test we've written before:
- The test is created thanks to
Test.fuzzinstead ofTest.test Test.fuzztakes an additional parameter which is a Fuzzer function (Fuzz.int) to generate a randomInt- The value is retrieved in the lambda function
\randomValue
Random values are generated thanks to Fuzz function like Fuzz.int or Fuzz.string. You can create your own if you need to.
Running the test will generate this output:

Note:
- The test was executed generating 100 random values by default (see
--fuzzoption) but it can be overridden - You can replay the test with the same exact value using the
seedvalue
Create a fuzz test to assert the following property: providing a random String variable, when calling the String.reverse function twice, the result should be the same as the original value.



