An introduction to the Elm programming language
Short version
Authors:

This workshop comes in two flavors:
-
a full version, in which you need to install elm and a few tools on your computer, using npm. In this version you will use the editor of your choice, and learn how to write unit tests, in addition to create an app. You will need around 3 hours to complete this version. If you don’t have enough time now, don’t worry, everything is provided in order you can finish it later.
-
a short version, in which you don’t need to install anything on your computer. You will use an online editor. This environment does not allow executing unit tests. So the last part of the full tutorial, which adds new feature in a TDD way, will be skipped. That’s why this version will be a bit quicker to complete than the full one.
This document is the material for the short version.
1. Workshop objectives
After this workshop, you should be able to create a basic Elm application.
1.1. The content of the workshop
-
practical: GUI of an image search engine
-
introduction to the language and tools
-
first steps with elm
-
basic principles of functional programming
-
the architecture of an elm application
-
client / server communication
2. Introduction to Elm
2.1. The Elm language
-
Elm is a functional language, whose compilation generates Javascript.
-
It has very strong typing

Image : User Henryhartley on en.wikipedia - license GFDL or CC-BY-SA-3.0, via Wikimedia Commons
Elm means elm, like the tree ("orme" in French), it’s not an acronym, so it’s written in lowercase.
Elm is one of the simplest functional languages available today. This translates into a limited number of concepts in the language. This simplicity is due to the will of the creator of the language to make it accessible to the greatest number of people.
The notion of type is more advanced than in Java for example. The compiler is able to infer the type of all expressions.
2.2. A front-end language

![]() |
development in elm |
![]() |
compilation & optimization |
Elm is a front-end language only (for the moment), it ultimately produces front-end javascript code.
The developer writes in elm, tests in elm.
The elm compiler compiles and optimizes the elm code into a concise and efficient javascript code.
This code is not intended to be read by a human.
The elm code is not deployed in production, only the compiled Javacript is.
2.3. How do you use it?
-
compile elm to JS
-
include in the HTML page the file resulting from the compilation
-
call the init() function
![]() |
It is possible for an elm app to interact with the native JS code. |
3. Editor
You will use Ellie, which is an online Elm development environement.
It is intended to be used for initiation to Elm, or to share executable elm snipets.
When you open Ellie, it contains an example app. You can execute it by clicking the compile button, and see the result on the right part of the screen.
Just clear the app in the top left part to be ready to start the workshop.
4. Hello, World!
4.1. A basic Elm application
Copy this code to the editor:
module Main exposing (main) --
import Html exposing (text) --
main =
text "Hello, World!" -- 
![]() |
Declares the Main module, and makes public the main function |
![]() |
Imports the "text" function of the Html module. |
![]() |
The main function invokes the text + function
and passes it a string as a parameter
|
4.2. Compile the application
Just click the Compile button
-
The result of the compilation is a single JS file, regardless of the number of elm source files.
-
We never edit this file, whose highly optimized content is not designed to be read.
In Ellie, you won’t see the resulting JS file. It will be automatically included in the html page you can see on the bottom left part of the screen.
4.3. Format the code
Use the button

to format the code.
4.4. Look at index.html
Ellie allows you to customize the Html page which will load the Elm app. Il looks like this:
<html>
<head>
<style>
/* you can style your program here */
</style>
</head>
<body>
<main></main>
<script>
var app = Elm.Main.init({ node: document.querySelector('main') })
// you can use ports and stuff here
</script>
</body>
</html>
![]() |
The HTML node in which the application will be displayed |
![]() |
Launching the elm application |
The init function takes as a parameter the reference of the node in which the application should be displayed
If you were not using Ellie, you whould have to add something like <script src="elm.js"></script>
to load the JS file resulting from the compilation.
4.5. Expected outcome
In the right part of the screen, you should see:

5. First Steps
5.1. Produce some Html
module Main exposing (main)
import Html exposing (h1, text)
view =
h1 [] [text "elm Image Search"] --
main =
view -- 
![]() |
call to function h1 of the Html module |
![]() |
call the view function |
![]() |
NB: added code is in bold type in all code blocks. |
Html has functions for each of the HTML language tags: div, p, etc.
These functions take two parameters: a list of attributes (class, onClick, etc.)
and a list of children.
In the above example h1 has no attribute and has a single child, of the text type.
5.2. Add an input field
module Main exposing (main)
import Html exposing (..) --
import Html.Attributes exposing (type_)
view =
div []
[ h1 [] [text "elm image search"]
, input [ type_ "text"] []
]
main =
view
![]() |
exposing (..) imports all functions and types of the module |
To avoid a conflict with the keyword type
,
the "type" attribute of the HTML language is generated
by a function named type_
On the other hand, there is no problem for the name of the attribute class
5.3. Expected result

6. Styles
6.1. Add a stylesheet
In the Html part of the editor, add the loading of the Bulma CSS framework
<html>
<head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.4/css/bulma.min.css">
</head>...
![]() |
|
6.2. Add CSS classes
module Main exposing (main)
import Html exposing (..)
import Html.Attributes exposing (class, type_)
view =
div [ class "container" ]
[ h1 [ class "title" ] [ text "elm image search" ]
, input [ type_ "text", class "medium input" ] []
]
main =
view
7. Functional programming: the basics
You may want to go directly to Type annotations if you’re already familiar with functional programming basics: pure functions, high-order functions, immutability, map/filter/reduce, lambdas…
7.1. Definition
In computer science, functional programming is a programming paradigm —a style of building the structure and elements of computer programs— that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data. It is a declarative programming paradigm, which means programming is done with expressions or declarations instead of statements.
source: Wikipedia
7.2. Immutability
Everything in Elm is immutable
This means a value or function cannot be modified after its creation
{-- no keyword var, val or def
everything is immutable value, even the functions --}
myNumber = 42
increment x =
x + 1
![]() |
You never change the value of a variable, it is not possible. For example, adding an item to a list is done by reusing the old list. The new item simply points to the first item in the old list, which is not modified. |
7.3. Higher order functions
Functional languages allow the use of higher-order functions.
A function is said to be of higher order when it takes at least one function as a parameter, and/or returns a function as a result.
7.4. Pure functions
A function is said pure if it does not produce any side effect.
The result of the call to a pure function depends only on the value
of its input parameters, and does not modify anything other than its return value.
This is why, for example, in elm an if
must be followed by an else
.
This has advantages:
-
it’s easy to test
-
it’s often easy to understand
-
it’s powerful in case of multithreading (which Elm does not allow us, because it is based on JS)
All Elm functions are pure. It means:
-
they are not based on any external state
-
they do not produce any side effects
-
they must return a result
7.5. List functions
Immutability implies you can’t do a for
loop in Elm : you cannot change the index value at each iteration.
Instead why you will use some functions applicable to lists, typical of functional programming:
-
map
to apply a transformation to each item in the list; -
filter
to extract from the list items matching a given criteria; -
fold
(namedreduce
in some other languages) to build a new data structure from the list.
7.6. Lambda
Lamba = anonymous function
Example:
List.map (\x -> x + 1) [1, 2, 3]

A lambda is a function that has no name.
In the example, the lambda is applied to each element of the list, by the map function of the List module. It will add 1 to each item in the list. x is the value of the current element. The result will be a list containing [2, 3, 4].
![]() |
In elm the "antislash" character is used to introduce an anonymous function. |
8. Type annotations
Type annotations are very important in elm, although they are optional. In addition to documenting your code, they help the compiler to provide clear user messages.
The type annotation defines the return type of a function. It’s written just above the function name, like:
increment : Int -> Int
increment x =
x + 1
A type annotation in Elm is:
-
the name of the function or value;
-
the colon symbol;
-
a series of types separated by arrows.
The last type is the return type.
increment : Int → Int
means the function increment
takes an Int as a parameter and returns an Int.
A type name in Elm always starts with an uppercase letter.
sum : Int -> Int -> Int -- takes two Int as parameters, and returns an Int
sum a b = -- a and b are integers
a + b
Some types are parametrized. By example:
wordCount: List String -> Int -- takes a list of strings as parameter,
-- and returns an Int
wordCount words =
...
A type maybe generic. In this case, the type name will use one or more lowercase tags.
By example List a
is a generic list (similar to List<Any>
or List<T>
in some other languages).
List.length: List a -> Int -- takes a list of any type as parameter,
-- and returns an Int
List.length myList =
...
You can chose any name for the tags, the only constraint is to start with a lowercase letter. It allows to give an indication of what this type should contain.
view: Html msg -- takes no parameter,
-- and returns some Html which can emit any kind of message
-- or no message at all
view =
text "Hello, World!"
Parentheses in an annotation type identify a function. Some examples:
apply: String -> (String -> List Int) -> Bool -- takes a String
-- and a function
-- which converts a String
-- to a list of integers,
-- and returns a boolean
selectTransformation: String -> (Int -> Int -> Int) -- takes a String
-- and returns a function
-- which takes two integers,
-- and returns an integer
9. The Elm Architecture
The Elm Architecture is often refered as "TEA"
9.1. The MVU pattern

Image by Kolja Wilcke
The principle of the MVU pattern is as follows:
-
the application includes a template, a view function and an update function.
-
The view function is in charge of rendering the model (it will generate Html)
-
The view issues messages, for example to indicate that the user has modified the content of an input field or clicked on a button;
-
The update function processes these messages and updates the model accordingly
-
This pattern is called MVU : Model - View - Update
9.2. The MVU pattern by example

-
the model contains the state of the application (i.e. it is a store). Initially:
deleteModal=False
This model is sent to theview
function by the elm runtime when the application starts. -
the
view
function uses the initial template to decide whether or not to display the modal (i.e. delete confirmation pop-up) -
the user clicks on the "delete" button on the html page.
-
the view then issues a message indicating this click
-
the runtime invokes the update function, and transmits to it the current model as well as the message sent by the view.
-
the update function contains the logic to be applied for each message
forDeleteClicked
, the logic consists in storing in the model with the new value of the fielddeleteModal
for this purpose, the update function creates a new model, which is a copy of the old one, but in which it gives the valueTrue
to the fielddeleteModal
. -
the runtime invokes the view function, and passes it the model it must render.
-
the new template contains
deleteModal=True
, so the view function displays the modal allowing the user to confirm the delete.
9.3. Let’s make the runtime visible!

9.4. Typical structure of an elm file
-- import section
import ...
-- main function
main = main
{-- Type declaration --}
type Model....
update function and sub-functions of the update --}
update = ...
{--view function and view sub-functions--}
view = ...
The MVU is implemented in the main file of the application.
Due to the declarative nature of the language, it is not a problem in elm to create longer file than in some other languages. Here the sections are separated by multi-line comments.
9.5. Implementation of the elm architecture
TEA uses Browser module
module Main exposing (main)
import Browser
import Html exposing (..)
import Html.exposing attributes (class, type_)
...
![]() |
The Browser module exposes four functions that implement the elm architecture. These functions depend on the type of application you want to create. The Browser module documentation details them. |
9.6. Define a type for the model
All the functions of the Browser module require a model.
Our model will be a simple chain of character.
import Html.Attributes exposing (class, type_)
type alias Model =
String
initialModel : Model
initialModel =
""
alias type
, as its name suggests, allows you to define a synonym for a type.
![]() |
initialModel is the variable - or function, it’s the same in elm, which defines the initial value of the model (here an empty string) Often this function is called |
9.7. Invoke Browser.sandbox
Browser.sandbox takes a record as a parameter
main =
Browser.sandbox
{ init = initialModel
, view = view
, update = update
}
A record is the name in elm for a structure.
The record expected by the sandbox
function must have three fields:
-
init, whose value is that of the initial model of the application
-
view, whose value is the reference of the function in charge of rendering the model visually. In our code this function is called view.
-
update, whose value is the reference of the function that updates the model. This function is usually called update.
It gives the "anchor points" of the runtime.
9.8. Define the events emitted by the view
Create a custom type
import Html.Attributes exposing (class, type_)
type Msg =
InputChanged String
type alias Model =
String
The Msg type is a custom type that will define the different messages that can be sent by the view, and processed by the update
function.
The declaration of a custom type lists the different constructors that can create a value of this type. For example, a Boolean type would be described by : True | False
For the moment there is only one case in which a message is sent. We will name the type constructor InputChanged
, because a message will be sent when the content of the input field has been modified. As with a function or variable name, any name can be chosen. It only needs to be unique and begins with a capital letter.
Each type constructor can accept 0, 1 or more parameters. It will be useful for us to know the new content of the input field. This is why we indicate that the InputChanged constructor takes a String type value as a parameter.
The result of a call to InputChanged
is the creation of a message of type Msg
.
9.9. Type annotations
Add a type annotation to the main function.
main: Program () Model Msg
main =
-
Adding type annotations documents the code, and allows the compiler to display more relevant error messages.
-
The main function does not take any parameter, and returns a Program () Model Msg, since it just calls Browser sandbox.
-
The brackets in the type annotation of the main function represent an empty tuple, called
unit
. It is an empty value. -
In the Browser Sandbox documentation, "msg" starting with a lowercase letter is not a type but a label. It is equivalent to a generic type (like <T> in some other languages). Ditto for model.
9.10. Detection of events on the view
The Html.events module provides functions to react to user actions.
import ...
import Html.Events exposing (onInput)
...
view : Model -> Html Msg --
view model =
div [ class "container" ]
[ h1 [ class "title" ] [ text "elm image search" ]
, input [ type_ "text"
, class "medium input"
, onInput InputChanged ] []
]
![]() |
Modify the type annotation to comply with Browser.sandbox requirements |
Each time the content of the input field is modified, the view will issue an InputChanged
message with the new value of the input field as a parameter.
Html
is a type constructor, which takes a message type as a parameter.
This type of annotation can be read as "The product view function of the Html that can send messages". And not just any messages: only those described by the Msg type we have defined.
9.11. Create an update function
view =
...
update: Msg -> Model -> Model
update msg model =
model
The update function will receive in first parameter a message sent by the view.
update
takes as second parameter the current model, and returns the new model in result.
The simplest possible implementation for update is to return the current model, without modifying it. That is what we are doing.
![]() |
When we say that the function update updates the model, it is for convenience, as the model is immutable. In reality, this function produces a new model, possibly from the old one, every time it is invoked. |
9.12. Update the model
update
must update the template according to the content of the message. The latter is analyzed using pattern matching:
update: Msg -> Model -> Model
update msg model =
case msg of
InputChanged value ->
value
If the message is of the InputChanged type, the new template will be equal to the value conveyed by the message (i.e. the content of the input field).
Pattern matching (or "pattern filtering") is a control structure which allows you to choose one of several behaviors, by looking at how the message was constructed. It will also allow to deconstruct the content of the message (unboxing) to recover its value.
In our example, msg can only have been built by calling "InputChanged" followed by a String for now.
9.13. Time-Travelling Debugger
Enter several characters in the input field, then click the DEBUG button on the top right to access the debugger. The OUTPUT button hides the debugger.
In the panel that appears, you can see each message that has been sent.
Clicking on a message returns the application to the state it was in when it was sent.
In the right part we see the state of the model after processing the message by the update
function.
![]() |
The history can be exported and then imported on another workstation. This makes it possible, for example, to replay and analyze a scenario that caused a bug on a colleague’s workstation. |
Click on resume
to resume the execution of the application, otherwise it will be frozen.
9.14. Let’s summarize: the elm architecture

The elm architecture is the structure of applications in this language. It is integrated into the language from the beginning and therefore eliminates the need for a framework.
-
the browser emits an event (following an action by the user for example)
-
the runtime, which is subscribed to this event, invokes the update function and passes it the current model + a message describing the event
-
the update function generates a new model
-
the runtime invokes the view function and passes it the new model
-
view generates a new virtual DOM and sends it back to the runtime
-
the runtime updates the real DOM
The code for this step is available here
10. Communicate with a server
10.1. Side effects
-
Side effects are managed by the runtime
-
Side effects are invoked via commands
-
The result of a side effect is provided via a message
We can only write pure functions in elm. The result of calling a function with given parameter values will ALWAYS return the same value - which is convenient for testing.
But what if you have to do an operation with a different result? For example, generate a random number? Or make an http request?
In this case we will have to issue a command that will ask the elm runtime to execute the impure function. The runtime will provide the result of executing this impure function by invoking the update function. The message may contain, for example, the random number generated.
10.2. Elm architecture with side effects

Compared to the simplest version of the elm architecture, we will have additional notions to manage the processing that causes side effects.
-
the update function generates a new model, and possibly a command
-
the runtime executes the command. This is where all side effects are processed (HTTP request, random number generation, etc.). The commands are asynchronous.
-
when the execution of a command ends, the runtime invokes the update function, and passes the current model a message that describes the result of the execution of the command (success or failure, value, etc…) ⇒ return to step 1
10.3. Change application type
Replace Browser.sandbox with Browser.element and compile
main =
Browser.element
{ init = init
, view = view
, update = update
}
Browser.element, unlike Browser.sandbox, allows you to interact with the world outside the application. Browser.element takes a record as a parameter with an additional field, subscriptions. The compiler clearly indicates which field is missing.
10.4. Implement subscriptions
Provide a minimum implementation of subscriptions to Browser.element :
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.none
main =
Browser.element
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
subscriptions
is a function that allows you to subscribe to function calls from the JS or elm runtime,
like the ticks of a clock.
In the immediate future, we will provide a subscription implementation that does nothing.
It will therefore return Sub.none, regardless of the value of the model passed to it as a parameter.
10.5. Adapt the initialization function
Replace initialModel
by a function named init
:
init : () -> (Model, Cmd Msg) --
init _ = --
("", Cmd.none) --
...
main =
Browser.element
{ init = init
![]() |
() = Unit = empty tuple = no value |
![]() |
_ = unused parameter |
![]() |
tuple (initial model, no command) |
The compiler reports that the functions expected by Browser.element have
signatures different from those used by Browser.sandbox
init and update must return a tuple with a Model record and a command.
The initialization function must also accept a flag as the first parameter. Flags are values that can be transmitted by the JS code when initializing the elm application (by adding a flags property to the object passed as a parameter to init())
For now, we don’t need a flag, so it will be initialized to Unit.
Unit is just an empty tuple. It is used to represent an empty value.
10.6. Adapt the update function
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
InputChanged value ->
(value, Cmd.none)
As with the init function, update must now return a tuple, whose first element is the new value of the model, the second is the command to execute. Cmd.none tells the elm runtime that there is no command to execute.
In the rest of the practical we will introduce a command that will launch an HTTP request.
10.7. Add a form
The form tag around the input field is used to detect the validation.
div [ class "container" ]
[ h1 [ class "title" ] [ text "elm image search" ]
, viewForm
]
viewForm : Html Msg
viewForm =
form []
[ input
[ type_ "text"
, class "medium input"
, onInput InputChanged
]
[]
]
To maintain the readability of the view
function,
the code that generates the form is extracted into a new
function named viewForm.
10.8. Declare a new message constructor
Add a message constructor to be used when the form is subbmitted:
type Msg
= InputChanged String
| FormSubmitted
The compiler will help you for the next steps.
A message can now be either an InputChanged or a FormSubmitted.
Gradually the possible types for a message will get richer.
They will represent all possible actions on the page.
The different message constructors do not necessarily have the same types of parameters, or even have no parameters as is the case for FormSubmitted.
10.9. Detect the validation of the form
-
import the
onSubmit
function of theHtml.events
module -
send a message
FormSubmitted
when the form is validated -
make sure that the application compiles
10.10. Perform an HTTP GET request
Install the elm/http module:
Click

to display the package panel, then search for the http module and install it. Click the same button again to hide the package panel.
The documentation of the Http module gives the type annotation of the get function:
get :
{ url : String
, expect : Expect msg
}
-> Cmd msg
10.11. Send an image search request
For now we will treat the answers as text.
FormSubmitted ->
let --
httpCommand = Http.get
{ url =
"https://unsplash.noprod-b.kmt.orange.com"
++ "/search/photos?query="
++ model
, expect = Http.expectString ResponseReceived --
}
in
( model, httpCommand )
![]() |
declaration of local variables |
![]() |
expectString indicates the type of response expected |
let … in allows you to declare local variables usable in the return value of the function.
ResponseReceived is the message that will be sent following the reception of the response. We will define this type constructor in a further step.
![]() |
Here again it is good to let yourself be guided by the compiler. |
10.12. Digression: the Result type
The Result type represents a response to an operation that may fail. It has two constructors: one to encapsulate the result of a successful operation, one to encapsulate an error.
type Result error value
= Ok value
| Err error
is equivalent to:
type Result a b
= Ok b
| Err a
Remember, lowercase names are just tags, not types.
10.13. Declare the type of the response
The update function must process the responses to the HTTP request.
type Msg
= InputChanged String
| FormSubmitted
| ResponseReceived (Result Http.Error String)
The HTTP module documentation tells us how it is used and the format of the response messages.
The message sent by the runtime when receiving a response to an HTTP request includes a Result type parameter. This type is itself configured. The first parameter of the type Result indicates the type that the result will have in case of failure (here Http.Error). The second parameter indicates the type of the result in case of a successful query (here a string).
(Result Http.Error String) is in brackets because it is the set that forms a type. Without brackets, this would mean that the ResponseReceived
type has three parameters, of the respective types Result
, Http.Error
and String
-which is not consistent with the operation of the Http module. Thanks to the parentheses, we indicate that ResponseReceived
has only one parameter, of type Result Http.Error String
.
![]() |
Continue to be guided by the compiler. |
10.14. Process the answer
The compiler tells us that a case is not processed by the update function:
case msg of
InputChanged value ->
...
FormSubmitted ->
...
ResponseReceived _ -> --
(model, Cmd.none)
![]() |
For the moment, we don’t know the answer |
It is necessary to add a branch to the box to process the new case.
The compiler requires that all cases be processed. There can be no forgetting. It is thanks to this type of principle that an elm application never causes an error at runtime.
In the code above, the response conveyed by the response message is ignored. The model is therefore returned unchanged.
![]() |
The compilation must be successful. |
10.15. Enrich the model
To display the server response, the model must contain the value entered by the user and the server response.
type alias Model =
{ searchTerms : String
, response : String
}
Changing the model type causes compilation errors. The compiler then tells us what changes to make to adapt the rest of the code. It is a typical elm experience. We are talking about Compiler Driven Development.
In the record type declaration the symbol :
is used to separate the keys from the types (as in all type declarations in elm).
The searchTerms
field will be used to store the character string entered by the user.
10.16. Initialize the new model
The initial value of the record fields is an empty string
There is no null value in elm.
init : () -> ( Model, Cmd Msg )
init _ =
( { searchTerms = ""
, response = ""
}
, Cmd.none
)
![]() |
In the record initialization the symbol |
10.17. Use the record in the update function
InputChanged value ->
( { model | searchTerms = value }, Cmd.none ) --
FormSubmitted ->
let
httpCommand =
Http.get
{ url =
"https://unsplash.noprod-b.kmt.orange.com"
++ "/search/photos?query="
++ model.searchTerms --
, expect = Http.expectString ResponseReceived
}
in
( model, httpCommand )
![]() |
model update |
![]() |
access to the value of the searchTerms field |
When we receive an InputChanged message, we will update the searchTerms field of the model to store the value entered by the user.
More precisely, as in elm everything is immutable, we will create a new model model, based on the old one, in which the searchTerms field will have a value equal to that of the value
parameter of the InputChanged
message.
![]() |
For convenience, we say that we update the model, but from a theoretical point of view, it is an abuse of language. |
10.18. Update the model based on the response received
case msg of
InputChanged value ->
...
FormSubmitted ->
...
ResponseReceived (Ok jsonString) ->
( ..., Cmd.none ) --
ResponseReceived (Err _) ->
( ..., Cmd.none ) -- 
![]() |
Store the received value in the response field of the model |
![]() |
Store an error message in the response field of the model |
A Result object can be of the type OK
or of the type Err
.
If it is of type OK
, its parameter will be the body of the Http response returned by the server.
If it is of type Err
, its parameter will be an Http error, which should be analyzed with pattern matching to display a specific error message.
For the time being, we will store a generic error message, hard-coded in the response field of the model.
![]() |
You can verify the request being made and its response using the developer tools of your Web Browser You can also use the Elm debugger to verify the messages processed by the update function and the resulting model. |
10.19. Display the answer
-
create a
viewResponse
function -
viewResponse
takes the model as a parameter and returns Html -
use
viewResponse
to display the server response or error message under the input field
Temporarily remove the query parameter from the request to cause an error 400, or disconnect your computer from the network.
11. Decoding JSON
11.1. Function Application Operator
To convert the Json to elm data structures, we will need the operator |> :
-
<| means: evaluate the expression on the right, pass its value to the left
-
|> means: evaluate the expression on the left, pass its value to the right
viewUsername : User -> Html Msg
viewUsername user =
div [] [ text <| user.firstname ++ " " ++ user.name ] -- 
![]() |
is equivalent to
|
These operators allow a very elegant syntax for chaining calls (ex.: apply map then filter then sort to a list)
11.2. Response from the server
The server response is in the form:
{
"results" : [
{
"urls" :
{
"thumb" : "http://...",
"regular" : "http://...",
...
}
}
]
...
}
The API response to an image search request is a JSON object that has a results
property.
The value of results is an array of objects describing images.
Each of these objects has a url property, which itself has thumb
and regular
properties that correspond to the image sizes we are interested in.
11.3. Declare the type to be received
-
install the
elm/json
package -
install the package
NoRedInk/elm-json-decode-pipeline
-
Add this code:
import Json.Decode as Decode exposing (Decoder, field, int, list, string)
import Json.Decode.Pipeline exposing (required, requiredAt)
type alias Image =
{ thumbnailUrl : String
, url : String
}
imageDecoder : Decoder Image --
imageDecoder =
Decode.succeed Image
|> requiredAt [ "urls", "thumb" ] string --
|> requiredAt [ "urls", "regular" ] string --
imageListDecoder : Decoder (List Image) --
imageListDecoder =
field "results" (list imageDecoder)
![]() |
imageDecoder converts the JSON received from the server to an Image type |
![]() |
the content of urls.thumb will go in the 1st field of the record |
![]() |
the content of urls.regular will go in the 2nd field of the record |
![]() |
imageListDecoder transforms the "results" field of the JSON into a list of Image records,each of which is created using imageDecoder .
|
It is essential in the decoder declaration to respect the order of the fields in the Image type.
11.4. Change the type of message received
type Msg
= InputChanged String
| FormSubmitted
| ResponseReceived (Result Http.Error (List Image))
The answer will be of Image list type and no longer of String type
11.5. Use the decoder
{ url =
"https://unsplash.noprod-b.kmt.orange.com"
++ "/search/photos?query="
++ model.searchTerms
, expect = Http.expectJson ResponseReceived imageListDecoder
}
expectJson
takes one more parameter than expectString
: the decoder to use to convert the JSON response into an elm data structure.
11.6. Storing images in the model
-
Delete the
response
field of typeModel
. -
Comment or delete the response display code
-
Add an
images
field to theModel
type -
Store the server response in case of success
-
Add a
message
field to theModel
type -
Store the error message when necessary
-
Display this message, if any, under the input field
-
Simulate a network problem to test the display of the error message
-
Check the correct functioning of the image list part with the debugger
![]() |
If there is no error, use If you want to use the
|
The code for this step is available here
12. Displaying responses
12.1. Show thumbnails
view model =
div [ class "container" ]
[ h1 [ class "title" ] [ text "elm image search" ]
, viewForm
, viewMessage model
, viewResults model
]
viewResults : Model -> Html Msg
viewResults model =
div [] (List.map viewThumbnail model.images)
viewThumbnail : Image -> Html Msg
viewThumbnail image =
img [ src image.thumbnailUrl ] []
List.map
applies the viewImage
function to each of the elements in the model.images
list, and returns a list containing the results.
![]() |
In functional languages, it is not possible to modify an index value at each iteration, that’s why there is no for or while loop. |
12.2. Improving the presentation of images
-
add the CSS classes "columns" and "is-multiline" to the div that includes the list of images
-
display each thumbnail in a div having the CSS classes "column" and "is-one-quarter"
-
add a vertical spacing between the form and the answers
12.3. Expected result

12.4. Improving the display of the error message
-
use a Bulma notification to improve the presentation of the error message
-
a click on the button must hide the notification
-
the launch of a new search must hide the notification
![]() |
HTML for displaying a notification with Bulma: <div class="notification is-danger"> <button class="delete"></button> Lorem ipsum dolor sit amet, consectetur </div> |
13. Conclusion
This is the end of this first approach to Elm.
We hope you start seeing what makes Elm a so delightful language:
-
reliable applications: no errors in production
-
simplicity of the tools
-
useful error messages
-
fast compilation
-
packages cohesion: no conflict between packages
-
performance: it compares to the fastest JS frameworks, an is better than the most popular of them (source)
-
light weight of the JS compiled files (source)
-
welcoming community (try Slack)
-
last but not least, refactoring without risk, which probably is its killer feature.

14. Going Further
If you want to go further, there are books and online resources:
-
Book: Programming Elm by Jeremy Fairbank
-
Book: Practical Elm for a Busy Developer: you need to practice a bit before reading this one, it’s not for beginners, but I definitely recommend it
-
Support: the #beginners channel on elm slack
-
Newsletter: Elm Weekly
-
Forum: Elm Discourse
-
Conference: Elm Europe, 27-28th of June 2019, Paris
You can also complete the app you started here, by example by displaying the large version of the image when clicking a thumbnail.