Skip to content

Instantly share code, notes, and snippets.

@cies
Last active March 5, 2021 11:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cies/0a5bbb07ff6c4956f21ffac499ce5d56 to your computer and use it in GitHub Desktop.
Save cies/0a5bbb07ff6c4956f21ffac499ce5d56 to your computer and use it in GitHub Desktop.
A beginner's introduction to Elm (0.19.1)

Elm: Elegant browser programs

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.

A minimal introduction to HTML

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 and alt 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!

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

A superficial look at an Elm app

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 }

An introduction to Elm

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.

Comments

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?
-}

Literal data types

Bool

The data type Bool holds a value that is either True or False. It is the smallest data type.

Int and Float: the number types

> 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), and
  • Float (numbers with a decimal dot, floating-point numbers).

String

In most programming languages strings are pieces of text.

> "hello"
"hello" : String

> "butter" ++ "fly"
"butterfly" : String

Try putting some strings together with the (++) operator.

Char

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

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.

Operators

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 on Int and Float.
  • - — Subtraction, minus, works both on Int and Float.
  • * — Multiplication, works both on Int and Float.
  • ^ — Power works both on Int and Float.
  • / — Division works only on Float.
  • // — Division works only on Int.
  • % — Modulo, rest from integer division, works only on Int.

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; evaluates True when equal otherwise False.
  • /= — Test for inequality; evaluates True when unequal otherwise False.
  • >True when left-hand value is greater than the right-hand value, otherwise False.
  • <True when left-hand value is smaller than the right-hand value, otherwise False.
  • >=True when left-hand value is greater than or equal to the right-hand value, otherwise False.
  • <=True when left-hand value is smaller than or equal to the right-hand value, otherwise False.

The logic operators (and a regular function):

  • && — Only True when left and right-hand values evaluate True, otherwise False.
  • ||True when either left or right-hand values evaluate True, otherwise False.
  • not — Not an operator function, but it fits the list :) Turns True into False 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)

Anonymous functions a.k.a. lambdas

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

Local declarations with let-expressions

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.

Control flow

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.

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.

case-expressions

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.

Modules

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.

Types

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 a String.
  • The value False is a Bool.
  • The value 3 is an Int.
  • The value 3.1415 is a Float.

Types of functions

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 -> String

When given two arguments, like String.repeat 3 "ha", it evaluates to "hahaha".

By giving String.repeat only one argument, like String.repeat 3, it will evaluate into a new function! This function take one argument (a String) to evaluate to a value.

This is a powerful feature of Elm! You can quickly create your own functions like:

threeTimes = String.repeat 3

Type annotations

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:

  1. 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 an Int, but you gave a Bool!”
  2. 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!

Composite data types

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

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.

Records

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 with String.repeat where the dot was used to pick the repeat function from the String 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" }

List

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!

Maybe

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

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.

Modeling

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 type List 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.

Type aliases

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.

Further reading

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment