Elm is a programming language that compiles to JavaScript (the main language understood by web browsers).
Contrary to some other programming languages (like C, C++, Java or Rust), Elm is not "general purpose", it is designed specifically to create web applications.
What makes Elm unique:
- Easy to get started, as it...
- ...is a "small" language (not much to learn),
- ...has a complete and well integrated set of tools, and
- ...gives beginner friendly errors!
- Elm makes writing correct programs easy (by virtue of something called strong type safety).
- All Elm applications follow The Elm Architecture which makes reading other's apps easy.
Elm compiles to JvaScript means that the Elm code is transformed in JavaScript, the language your browser understands. For placing and styling the bits of text on a web page or creating UIs in web apps we use HTML and CSS. HTML needs CSS for styling, but HTML is perfectly usable by itself. When an Elm application runs in the browser, it feeds that browser HTML in order to draw things on the screen. So we start with small primer of HTML.
An HTML file is build from tags. Here an example that combines a text and an image:
<div>
<p>
My holiday
</p>
<img src="/picture.jpg" alt="Me hugging a palm tree">
</div>
The example starts with an all enclosing div
tag (opening with <div>
and closing at </div>
).
Tags most often come in pairs —open and close— that work like parentheses, enclosing their children.
The div
's first child is a p
tag with some text.
The div
's first child is an img
tag has what we call attributes.
The first attribute, src
, points to an image file; the second, alt
, providing a text alternative (for when the image did not load yet, or for screen readers).
So HTML tags have:
- A list of arguments, where each argument has a name (
src
andalt
in the example). - A list of children, where each child can be:
- a bit of text (like
My holiday
in the example), or - another HTML tag!
- a bit of text (like
HTML tags have what is called a tree structure: tags can have tags as children.
The root of the tree is the html
tag.
See for instance this minimal complete example of a valid HTML file:
<html>
<head>
<style> /* The CSS rules would go here */ </style>
<title>About me</title>
</head>
<body>
<div>
<p>
My holiday
</p>
<img src="/picture.jpg" alt="Me hugging a palm tree">
</div>
</body>
</html>
When creating a screen filling web app with Elm, as similar very small HTML file is used:
<html>
<head>
<style> /* The CSS rules would go here */ </style>
<script src="/elm.js"></script>
</head>
<body>
<main></main>
<script>
var app = Elm.Main.init({ node: document.querySelector('main') })
// you can use ports and stuff here
</script>
</body>
</html>
This HTML file loads the JavaScript that resulted from your Elm code (/elm.js
).
Then it starts the Elm app and binds it to the main
tag.
The application draws things on the screen by programmatically putting/changing tags in the main
tag.
You should now know enough about HTML tags to get started.
Later, as we build UIs in Elm, you will learn about button
and input
tags.
Just know that they are HTML tags, and that learn anything about any tag online.
The Mozilla Developer Network, known as MDN, is a great resource.
If you want to learn about some HTML tag, just search the web for: mdn input tag
Before we go into the details of the Elm programming language, lets first have a look at the code of a simple Elm application. This little app lets you increment and decrement a number with two buttons:
module Main exposing (main)
import Browser
import Html exposing (Html, button, div, text)
import Html.Events as E
type alias Model = { count : Int }
type Msg = Increment | Decrement
update : Msg -> Model -> Model
update msg model = case msg of
Increment -> { model | count = model.count + 1 }
Decrement -> { model | count = model.count - 1 }
view : Model -> Html Msg
view model =
div []
[ button [ E.onClick Increment ] [ text "+1" ]
, div [] [ text <| String.fromInt model.count ]
, button [ E.onClick Decrement ] [ text "-1" ]
]
main : Program () Model Msg
main = Browser.sandbox { init = { count = 0 }, view = view, update = update }
You may run and tweak this app with Ellie, all in the browser!
First we quickly walk through the code, just to get the drift. Then we move on to explain in detail how to read and write code like this!
It starts with a module definition, giving it a name (Main
) and exposing only one function (main
):
module Main exposing (main)
Then you find the import
statements, this makes functionality of other Elm modules (Browser
, Html
and Html.Events
) available to the code in this module:
import Browser
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)
To find the documentation of these modules you can follow the links in the text above, use a search engine, or use a code editor that understands Elm (these usually show documentation by mouse clicking on or hovering over the code).
The following two lines define data types, the Model
which "keeps the count" and the Msg
which specifies all possible "actions" this app support:
type alias Model = { count : Int }
type Msg = Increment | Decrement
This block of code defines the update
function, which transforms a value of type Msg
and a value of type Model
into a value of type Model
(it updates a Model
with a Msg
):
update : Msg -> Model -> Model
update msg model = case msg of
Increment -> { model | count = model.count + 1 }
Decrement -> { model | count = model.count - 1 }
First thing to note is that Elm has meaningful indentation and line breaks
The next up is the view
function. It renders (transforms) the just defined Model
(a counter) into HTML to be presented by web browser. It uses functions from the Html
module we imported earlier. Some of these functions, i.e. div
and button
correspond to HTML tags:
view : Model -> Html Msg
view model =
div []
[ button [ onClick Increment ] [ text "+1" ]
, div [] [ text (String.fromInt model.count) ]
, button [ onClick Decrement ] [ text "-1" ]
]
HTML and CSS are most commonly used to create interfaces for web browsers. The interface of this simple app only uses HTML.
The last two lines describe the main function which combines the view
and update
functions into a Program
, a runnable Elm application:
main : Program () Model Msg
main = Browser.sandbox { init = { count = 0 }, view = view, update = update }
Here we will go through the "meaning" (or semantics) and "form" (or syntax) of some of Elm's core language.
The primary building blocks of Elm code are values (data) and functions (operations on data). The values (data) can be further divided in primitive data types and composite data types (a.k.a. data structures).
The text has the following chapters:
- Comments — for fellow humans, maybe your future self.
- Literal data types — for example the values
42
,True
and"Hello!"
. - Functions — operate on and transform data.
- Control flow — decide what code gets executed.
- Modules — code organization and sharing.
- Types — data types and function types.
- Composite data types — tuples, records,
List
,Maybe
and custom types.
In programming code comments are added to make it easier for humans to understand. Compilers ignore the comments. Elm is no exception here. The Elm compiler (that transforms Elm code to JavaScript to be run in the browser) ignores comments.
Comments in Elm come it two forms (two syntaxes): single line comments (start with --
) and
multi-line comments (start with {-
and close with -}
). Here some examples:
-- singe line comment
numberThree = 3 -- single line comment after some Elm code
{-
Oh my beloved belly button.
The squidgy ring in my midriff mutton.
Your mystery is such tricky stuff:
Why are you so full of fluff?
-}
The data type Bool
holds a value that is either True
or False
. It is the smallest data type.
> 42 * 10
420 : number
> 1 / 4
0.25 : Float
Note: To try this yourself use
elm repl
(start it from the command line if you have Elm installed). For an online version see the black box in this guide. REPL stands for run evaluate print loop, it is a kind of live coding environment.
Try typing in things like 30 * 60 * 1000
and 2 ^ 4
. It should work just like a calculator!
As we saw 42 * 10
evaluates to 420
, and 1 / 4
evaluates to 0.25
. Note that 420
is a number
and 0.25
is of type Float
.
For now, it suffices to understand that Elm has two number
data types:
Int
(whole numbers, integers), andFloat
(numbers with a decimal dot, floating-point numbers).
In most programming languages strings are pieces of text.
> "hello"
"hello" : String
> "butter" ++ "fly"
"butterfly" : String
Try putting some strings together with the (++)
operator.
In Elm String
values break down into a list of Char
values.
One could say a Char
is a letter, or number, or special character.
Where a String
uses double quotes ("
), the Char
uses single quotes ('
)
> 'a'
'a' : Char
Functions transform values. They take in one or more values of specific types, called arguments. When the arguments are provided it evaluates to a value, or... in some cases they evaluate to other functions! This may be confusing at first, if that is the case then just remember that functions transform values.
For example, greet
is a function that takes one argument, name
, and evaluates to a string that says hello:
> greet name = "Hello " ++ name ++ "!"
<function> : String -> String
> greet \"Alice"
"Hello Alice!" : String
The type of the greet function is String -> String
, we'll explain how to read this in a later chapter.
Now let's what about a madlib
function that takes two arguments, animal
and adjective
?
> madlib animal adjective = "The ostentatious " ++ animal ++ " wears " ++ adjective ++ " shorts."
<function> : String -> String -> String
> madlib "cat" "ergonomic"
"The ostentatious cat wears ergonomic shorts." : String
> madlib ("butter" ++ "fly") "metallic"
"The ostentatious butterfly wears metallic shorts." : String
Notice how we used parentheses to group "butter" ++ "fly"
together in the second example. The parentheses instruct Elm to evaluate the expression "butter" ++ "fly"
and pass the result as the first argument to the madlib
function. Without parentheses the madlib
function cannot find its arguments which will result in an error.
Note: Those familiar with languages like JavaScript may be surprised that functions look different in Elm:
madlib "cat" "ergonomic" -- Elm madlib("cat", "ergonomic") // JavaScript madlib ("butter" ++ "fly") "metallic" -- Elm madlib("butter" + "fly", "metallic") // JavaScript
This can be surprising at first, but this style ends up using fewer parentheses and commas. Elm code looks very clean in comparison.
When being introduced to Int
and Float
you were —perhaps unknowingly— also introduced to several functions to manipulate values of these type. They were:
+
— Addition, plus, works both onInt
andFloat
.-
— Subtraction, minus, works both onInt
andFloat
.*
— Multiplication, works both onInt
andFloat
.^
— Power works both onInt
andFloat
./
— Division works only onFloat
.//
— Division works only onInt
.%
— Modulo, rest from integer division, works only onInt
.
Operators are just functions whose name consists only of special characters (+-*&^%$#@!~<>:/
) instead of letters and numbers as with non-operator functions. Operators are infix by default, this means the first argument comes before the operator. Add parentheses around the operator as if it is a regular function name (prefix).
2 + 2 -- operator as infix
(+) 2 2 -- operator as prefix
add 2 2 -- normal function as prefix
The compare operators:
==
— Test any two values for equality; evaluatesTrue
when equal otherwiseFalse
./=
— Test for inequality; evaluatesTrue
when unequal otherwiseFalse
.>
—True
when left-hand value is greater than the right-hand value, otherwiseFalse
.<
—True
when left-hand value is smaller than the right-hand value, otherwiseFalse
.>=
—True
when left-hand value is greater than or equal to the right-hand value, otherwiseFalse
.<=
—True
when left-hand value is smaller than or equal to the right-hand value, otherwiseFalse
.
The logic operators (and a regular function):
&&
— OnlyTrue
when left and right-hand values evaluateTrue
, otherwiseFalse
.||
—True
when either left or right-hand values evaluateTrue
, otherwiseFalse
.not
— Not an operator function, but it fits the list :) TurnsTrue
intoFalse
and vise versa.
The function composition operators:
|>
— The following code is equivalent:removeNotAllowed menuData |> renderMenu
renderMenu (removeNotAllowed menuData)
<|
— The following code is equivalent:renderMenu <| removeNotAllowed menuData
renderMenu (removeNotAllowed menuData)
We just seen a bunch of operator functions. And before that we saw how to make a greet
function.
All these functions have names, the greet
function is named greet
and the operator all have their own abstract name (e.g.: +
, -
, *
).
Elm also allows you to make functions with no name, they are called lambdas for historical reasons. Lambdas are very useful when you want to use the function immediately and only in one place.
Let's rewrite this named function: hypotenuse a b = a^2 + b^2
Into a lambda: \a b -> a^2 + b^2
Lambdas start with a backslash (\) followed by its arguments (a
and b
),
then an arrow to separate the arguments from the function body.
Lambdas have types, just like named functions, have a look:
> \a b -> a^2 + b^2
<function> : number -> number -> number
> \name -> "Hey " ++ name ++ " having fun?"
<function> : String -> String
So far all the values and functions we declared in Elm files we're top-level. Everything declared at top-level is available throughout the file (and possible beyond).
With let
expressions we can declare values and function locally.
Let's rewrite the madlib
function to use a let
-expression:
madlib animal adjective =
let
animalBit = "The ostentatious " ++ animal
shortsBit = adjective ++ " shorts"
combine a b = a ++ " wears " ++ b ++ "."
in
combine animalBit shortsBit
In this case it did not make a lot of sense to use a let expression, it just serves as an example.
animalBit
, shortsBit
and combine
are only available within the in
block of the let
-expression.
let
-expressions are very useful to unclutter long lines, or add some names (descriptions) to your code.
Elm has two ways to control the execution flow of the program: if
and case
expressions.
The case
expressions are more powerful, but also require more typing.
Let's start with if
-expressions.
With if
-expressions the flow of a program controlled with a value of type Bool
.
An if
-expression starts with if
followed by the boolean condition value,
the then
keyword followed by the true-clause, and finally
the else
keyword followed by the false-clause.
For example let's make a new greet
function that is appropriately respectful to Che Guevara:
> greet name =
| if name == "Che"
| then "Greetings comrade!"
| else "Hey!"
<function> : String -> String
> greet "Tom"
"Hey!" : String
> greet "Che"
"Greetings comrade!" : String
It is possible to chain if
-expressions like:
> greet name =
| if name == "Abraham Lincoln"
| then "Greetings Mr. President!"
| else if name == "Donald Trump"
| then "Grab 'm by the what?"
| else "Hey " ++ name ++ "!"
<function> : String -> String
If you want to chain many expressions you may want to look into case
-expressions (next chapter).
In some languages if
statements without an else
clause are valid, but not in Elm!
In Elm if
is an expression and thus should always evaluate to a value.
Where if
-expressions allow controlling execution flow over two clauses with a boolean,
case
-expressions allow controlling execution flow over many clauses and by many types of control value.
Between the case
and the of
keywords you find the condition.
On the next indented (!) lines you find the different clauses.
Each clause consists of (1) a value that is matched with the condition,
(2) an arrow, and (3) the code to evaluate when the condition matches.
Here an example of a function that uses a case
-expression with a condition value of type Int
.
fib : Int -> Int
fib n =
case n of
0 -> 1
1 -> 1
_ -> fib (n-1) + fib (n-2)
The last clause of this case
-statement has no number but an underscore (_
).
This is the catch-all clause.
Since case
-statement match from first to last clause, you often find a catch all as last clause.
In case the condition did not match any of the clauses by then, the last clause will be evaluated.
Elm requires case
-expressions to be exhaustive.
You need to cover all possible values of a type with the clauses; Elm will check you did not forget any.
You can always add a catch-all clause as a fall back.
During the deep dive we saw the following module
definition and import
statements:
module Main exposing (main)
import Browser
import Html exposing (Html, button, div, text)
import Html.Events as E
The module Main exposing (main)
means that the code of this file is know as the Main
module and only exposes the main
function (made available to those importing this module).
With import Browser
we import everything exposed by the Browser
module, but under the Browser
name. Later in the code of the deep dive we see Browser.sandbox
is used, this is the sandbox
function from the Browser
module. To be able to use sandbox
without the Browser.
prefix we have to expose it (as seen in the next import
statement).
In the import Html exposing (Html, button, div, text)
we expose the Html
data type and three functions —button
, div
and text
— to be used in our code without Html.
prefix.
Finally, in import Html.Events as E
we import Html.Events
as E
. The code of the deep dive example used the onClick
function from the Html.Evens
module, which it prefixes with E.
to get E.onClick
.
Let's enter some simple expressions and see what happens:
> "hello"
"hello" : String
> not True
False : Bool
> round 3.1415
3 : Int
Click on this black box above (the cursor should start blinking), then type in 3.1415
and press the ENTER key. It should print out 3.1415
(the value) followed by the Float
(the type).
Okay, but what is going on here exactly? Each entry shows value along with what type of value it happens to be. You can read these examples out loud like this:
- The value
"hello"
is aString
. - The value
False
is aBool
. - The value
3
is anInt
. - The value
3.1415
is aFloat
.
Let's see the type of some functions:
> String.length
<function> : String -> Int
Try other functions like round
or sqrt
, or operator functions like (*)
, (==)
or (&&)
, to see some more function types!
The String.length
function has type String -> Int
. This means it takes in a String
argument to evaluate into an Int
value. So let's try giving it an argument:
> String.length "Supercalifragilisticexpialidocious"
34 : Int
So we start with a String -> Int
function and give it a String
argument. This results in an Int
.
What happens when you do not give a String
though? Try entering String.length [1,2,3]
or String.length True
and see what happens...
You will find that a String -> Int
function must get a String
argument!
Note: Functions that take multiple arguments also have multiple arrows in their type. For example, here is a function that takes two arguments:
> String.repeat <function> : Int -> String -> StringWhen given two arguments, like
String.repeat 3 "ha"
, it evaluates to"hahaha"
.By giving
String.repeat
only one argument, likeString.repeat 3
, it will evaluate into a new function! This function take one argument (aString
) to evaluate to a value.This is a powerful feature of Elm! You can quickly create your own functions like:
threeTimes = String.repeat 3
So far we let Elm figure out the types (this is called type inference). We can also write type annotations to specify the types our selves. Have a look:
-- Here's the function from the previous section:
threeTimes : String -> String
threeTimes = String.repeat 3
-- This function divides a Float by two
half : Float -> Float
half n = n / 2
-- half 256 == 128
-- half "3" -- error!
-- Not a function, just a constant value
speedOfLight : Int
speedOfLight = 299792458 -- meters per second
-- This function transforms an Int to a String
checkPower : Int -> String
checkPower powerLevel =
if powerLevel > 9000 then "It's over 9000!!!" else "Meh"
-- checkPower 9001 == "It's over 9000!!!"
-- checkPower True -- error!
Elm does not require type annotations, but they're highly recommended. Benefits include:
- Error message quality — When you add a type annotation, it tells the Elm compiler what you are trying to do. Your implementation may have mistakes, and now the compiler can compare against your stated intent. “You promised argument
powerLevel
will be anInt
, but you gave aBool
!” - Documentation — When you revisit code later (or when a colleague visits it for the first time) it can be really helpful to see exactly what is going in and out of the function without having to read the implementation super carefully.
People can make mistakes in type annotations though, so what happens if the annotation does not match the implementation? The Elm compiler figures out all the types on its own and checks it matches your annotation. In other words, the compiler will always verify that all the annotations you add are correct. So you get better error messages and documentation always stays up to date!
We have already been introduced to literal data types adn their values, like: 42
(an Int
), True
(a Bool
) and "lolrotf"
(a String
). We will now look into the various ways we can compose these literal data types.
Tuples in Elm may hold either two or three values, called its fields. Each field can have any type. They are commonly used when a function evaluates to more than one value. The following function gets a name and gives a message for the user:
> isGoodName name =
| if String.length name <= 20
| then (True, "name accepted!")
| else (False, "name was too long; please limit it to 20 characters")
|
<function> : String -> ( Bool, String )
> isGoodName "Tom"
(True,"name accepted!") : ( Bool, String )
To create a tuple value simply put two or three comma-separated values between parentheses. In type annotations a tuple has the same syntax but with types instead of values. For example:
piDefinition : ( String, Float )
piDefinition = ("π", 3.141592653589793)
Tuples are very useful, but in more complicated situation it usually better to use records (explained in the next section) instead of tuples. This is why Elm only allows tuples of 2 or 3 values.
The record data structures are similar to tuples in that they allow value composition. Contrary to tuples, records can compose any positive number of values, also called fields. The fields a record consists of have names, this helps keeping track of the potentially large number of fields in a record.
Here we assign a record representing the British economist John A. Hobson to john
:
> john =
| { first = "John"
| , last = "Hobson"
| , age = 81
| }
|
{ first = "John", last = "Hobson", age = 81 }
: { first : String, last : String, age : Int }
> john.last
"Hobson" : String
In the example above we defined a record with three fields about John. We also see that the value of the last
field (a String
) is "accessed" with the dot (.
).
Note: Earlier we saw that the dot (
.
) was used to pick from a module. Like withString.repeat
where the dot was used to pick therepeat
function from theString
module. We should know the dot has more than one meaning in Elm:
- The decimal separator in numbers of type
Float
- Picking from a module
- Accessing a record's field
Since modules always start with a capital letter and decimals separators have numbers on each side, it's easy to know what meaning the dot has in various bits of code.
You can also access record fields using "field access functions" like this:
> john = { first = "John", last = "Hobson", age = 81 }
{ first = "John", last = "Hobson", age = 81 }
: { first : String, last : String, age : Int }
> .last john
"Hobson" : String
In Elm values are immutable, they cannot be changed. This is also the case for records. So if we want to change one or more fields in a record, instead of changing the values in an existing record we create a new record that contains the changes. For example (the types are hidden in this example):
> john = { first = "John", last = "Hobson", age = 81 }
{ age = 81, first = "John", last = "Hobson" }
> { john | last = "Adams" }
{ age = 81, first = "John", last = "Adams" }
> { john | age = 22 }
{ age = 22, first = "John", last = "Hobson" }
If you wanted to say these expressions out loud, you would say something like, "I want a new version of John where his last name is Adams" and "john where the age is 22".
So a function to update ages might look like this:
> celebrateBirthday person = { person | age = person.age + 1 }
<function> : { a | age : number } -> { a | age : number }
> john = { first = "John", last = "Hobson", age = 81 }
{ age = 81, first = "John", last = "Hobson" }
> celebrateBirthday john
{ age = 82, first = "John", last = "Hobson" }
Lists contain a sequence of values we call it's elements. It may contain any number of values: just one, many hundreds or no values at all (an empty list). In the following example we assign a list of String
values to friends
:
friends : List String
friends = ["Piotr", "Lena", "Friedrich"]
Lists may only contain values of the same type! So they either contain values of type String
or values of type Int
, but never a combination of both:
invalidList = ["the answer", 42] -- this is not valid Elm
The Elm language comes with a List
module (follow the link to read the documentation), which apart from the List
type also exposes functions to work with lists. Some examples:
> names = ["Alice", "Bob", "Chuck"]
["Alice", "Bob", "Chuck"] : List String
> List.isEmpty names
False : Bool
> List.length names
3 : Int
> List.reverse names
["Chuck", "Bob", "Alice"] : List String
> numbers = [4, 3, 2, 1]
[4, 3, 2, 1] : List number
> List.sort numbers
[1, 2, 3, 4] : List number
> increment n = n + 1
<function> : number -> number
> List.map increment numbers
[5, 4, 3, 2] : List number
Try making some lists and using functions like List.length
.
Remember: all values a list contains must have the same type!
The easiest way to approach Maybe
is by thinking of it as a List
that can have either zero or one element.
The Maybe
type is used to model the possible absence of a value.
A value of type Maybe Int
is either Just 3
(3
is an example here, it can be any Int
) or Nothing
.
For instance the String.toInt
function. It converts a bit of text to an Int
value.
Sometimes this fails, like with String.toInt "three"
as it can only convert numbers written as number, not when spelled out. That's where Maybe
comes in.
The type of String.toInt
is String -> Maybe Int
.
When the String
provided as argument cannot be converted it will simply evaluate to Nothing
.
When accessing the value that maybe contained in a Maybe
type, we need to explicitly deal with the possibility there is no value contained in there at all.
Custom types are one of Elm's most powerful features.
Let's start with an example.
We need to keep track of the privileges of the users in our web forum.
Some users have regular privileges and others, the administrators, can do more.
We can model this by defining a UserPrivilege
type listing all the possible variations:
type UserPrivileges
= Regular
| Admin
To model the administrator thomas
we use a record, like:
thomas : { id : Int, name : String, privileges : UserPrivileges }
thomas = { id = 1, name = "Thomas", privileges = Admin }
Now we could have used a Bool
to keep track is a user has administrator privileges or not.
The example would then look like:
thomas : { id : Int, name : String, isAdmin : Bool }
thomas = { id = 1, name = "Thomas", isAdmin = True }
In this example the meaning of the Bool
is only known from the field name of the record.
We could easily mistake confuse that value with another value of type Bool
,
and what if we want to add another category of user privileges later?
Creating a custom type is a great way to express very specifically what is allowed.
In the next example the UserPrivileges
are slightly more complex.
The forum got a lot of spam posts.
To counter this we only allow users that have accumulated some points by making comments to post to the forum.
A user with Admin
privileges does not need points, hence only the Regular
value keeps track of points:
type UserPrivileges
= Regular String
| Admin
thomas = { id = 1, name = "Thomas", privileges = Admin }
kate95 = { id = 1, name = "Thomas", privileges = Regular -42 }
As we can see, kate95
has some negative points and will not be able to post.
On the other hand thomas
has Admin
privileges and thus cannot even have points.
Custom types become extremely powerful when you start modeling situations very precisely. For example, if you are loading remote data in your application you may want to use te following from the RemoteData package:
type RemoteData e a
= NotAsked
| Loading
| Failure e
| Success a
To use this type we still need to fill in the type variables a
and e
.
Note: Earlier the
List
type also had a type variable to specify the type of the elements it may contain. The list of friends["Piotr", "Lena", "Friedrich"]
was of typeList String
(read as a list of strings).
The parameter a
represents type of the requested data, and e
represents the type of the error.
So you can start in the NotAsked
state, when the user clicks the "load more" button transition to the Loading
state, and then transition to Failure e
or Success a
depending on what happens.
A type alias gives a new (shorter) name to a type. For example, you could create a User
alias like this:
type alias User =
{ name : String
, age : Int
}
Rather than writing the whole record type all the time, we use User
instead.
It makes type annotations easier to read and understand:
isOldEnoughToVote : User -> Bool -- WITH ALIAS
isOldEnoughToVote user = user.age >= 18
isOldEnoughToVote2 : { name : String, age : Int } -> Bool -- WITHOUT ALIAS
isOldEnoughToVote2 user = user.age >= 18
These two definitions are equivalent, but the one with a type alias is shorter and easier to read. So all we are doing is making an alias for a long type.
- Official Elm documentation
- Beginning Elm, a nice introduction with illustrations
- Elm package index, find a library
- TodoMVC in Elm, code with link to live version