Quickstart Elm 0.19, part 4
Elm lang tips and tricks for beginners
By: Ajdin Imsirovic 23 October 2019
In this article series, we tried to get into Elm as quickly as possible, by buiding three mini apps up to this point. In this article we’ll shift our focus on some more theoretical concepts, before we move onto the more advanced stuff.
Note: Examples in this article use Elm 0.19.
Note that in this article, we’ll be using the online elmrepl, http://elmrepl.cuberoot.in exensively.
Tip #1: Convert HTML to Elm online
It is a nice exercise to try to convert some convoluted HTML code to Elm code by yourself.
However, there is an online converter available at: http://mbylstra.github.io/HTML-to-elm/.
We can simply paste in the HTML code to the left pane of the provided URL, and get the Elm representation in the right-hand panel on the website.
Tip #2: Function signatures for HTML elements
All HTML elements share the same function signature pattern:
<element> : List (Attribute msg) -> List (HTML msg) -> HTML msg
Every HTML element takes in two lists: a List of Attributes and a List of Children elements. Then, they return an Html msg
. If the value returned from a function does not emit a message, that code will return the msg
type.
In other words, every element returns the value of HTML. This HTML value is of type msg
, because they end up as plain HTML nodes and they will not (cannot!) change our app’s state.
That’s why it is perfectly possible to just keep writing Elm’s HTML functions in our main
function and never add the view
or update
, and still have a working web page. It is possible because we are rendering our code without any messages, so effectively, there is never going to be anything to update, and thus, we can do without the update
function. That’s why we can replace the implicit type of msg
with a
:
main : HTML a
By convention, a
stands for anything
. Since the main
function will never return a message, we can be more explicit about it, and have the following code as the function signature:
main : HTML Never
Now, we are explicitly declaring that we will never return a message. The Type of Never
cannot ever be constructed.
Tip #3: You don’t have to pass the view
code directly into the main
function
Instead, you can assign it to the view
function, which will then be passed to the main
function. This way, we are beginning to make our code more modular and reusable.
To do this, you’ll need to set an updated main
function:
main : HTML Never
main =
view
The referenced view is now on its own:
view : HTML Never
view =
A nice thing about this setup is that we can now pass our entire view
to a wrapping div
, for example, like this:
main : HTML Never
main =
div [] [ view ]
Tip #4: How type alias of Model works
By adding a type alias for our Model, we’ll make it easier to change our Model from something other than Int
(in the second article in this article series), if we ever decide to do so, which makes our code more maintainable.
type alias Model =
Int
Next, let’s update type annotations throughout the app:
model : Model
model =
...
update : Msg -> Model -> String
update msg model =
...
view : Model -> HTML Msg
view model =
...
It’s obvious what we are doing in the code: we are aliasing the value of Int
with the type alias Model
. It is very important to understand this and commit it to memory since this simple example shows exactly what type alias does and how it works.
Making these changes, we can now see an interesting pattern: the update
function’s type annotation uses the Msg
, with the capital M
, while we pass it the msg
, with the small m
, since it’s the first argument.
Similarly, we are passing the model
with the small letter m
to our view
, but in our view
function’s type annotation, we are referencing the Model
with the capital letter M
.
What’s going on here? The explanation is simple. We can think of the lowercase instances as simply generic labels, which can be anything. For example, consider the following changes to our code.
First, let’s replace the existing update function’s msg
with this:
update a model =
case a of
Next let’s replace the existing view
function’s model
with a
, as follows:
view a =
...
, p [ class "mt-5 lead" ] [ text a ]
]
]
]
The fact that we replaced both msg
and model
with the more generic a
in the preceding code, and did not break it, is great. The compiler happily performs its duties, and we still have a working app.
Type alias is used to make it easier to read complex type annotations. Also, type annotations are capitalized by default, and so are union types. Thus, both the Model
and the Msg
are capitalized in our code, and we cannot change them.
Tip #5: Primitive types in Elm
The primitive types in Elm include: Char
, String
, Bool
, and number
(Int
and Float
).
When we use single quotes, we get Chars
. To get the type of String
from a value, we need to surround that value in double quotes.
Multiline strings are written by enclosing any number of lines in three consecutive double quote characters.
If we just type a number, we’ll get back that same number, followed with a colon and the number
type:
> 5
5 : number
If we test a decimal number, we’ll get back the type of Float
:
> 3.6
3.6 : Float
To get back a value of type Int
from a decimal number, let’s run the following command:
> truncate 3.14
3 : Int
Boolean values are simple. Just make sure to capitalize (otherwise you’ll get an error).
> True
True : Bool
> False
False : Bool
Tip #6: Why are some types capitalized, and some are not?
Why are some types capitalized, and some are not? If a type is capitalized, it means it is an explicit type. Basically, the number
type is used for both Ints
and Floats
. Which one it will end up being (which explicit type it will end up being), depends on how that number is used. Put differently, number
is an implicit type, since it can end up as an explicit Int
or an explicit Float
.
Tip #7: Data structures: lists, tuples, records, sets, arrays, and dictionaries
Lists
A list in Elm is like an array in JavaScript. For our first example, let’s type this value in Elm REPL:
[ 1, 2, 3, 4 ]
This is what we get back from the REPL:
[1,2,3,4] : List number
Here’s a list of Floats:
[ 0.1, 0.2, 0.3, 0.4 ]
This is what we get back from the REPL:
[0.1,0.2,0.3,0.4] : List Float
What about an empty list?
> []
[] : List a
List a means that this list is empty, that is, that it can hold anything. This wraps up our short overview of Lists in Elm. Next, we will look at tuples.
Remember, mixing values in Lists in Elm will trow a TYPE MISMATCH error.
Tuples
In Elm, a tuple is a data structure that can hold values of various types.
To make a Tuple in Elm REPL, let’s simply put a String and a Boolean inside parentheses:
( "abc", True )
The REPL will respond with:
("abc",True) : ( String, Bool )
A tuple can hold a maximum of nine values. Interestingly, tuples of different lengths are considered to be of different types. For example, let’s make a List that holds two tuples, using Elm REPL:
[ ( 'a', 'b' ), ( 'c', 'd' ) ]
This expression will evaluate to:
[('a','b'),('c','d')] : List ( Char, Char )
What REPL tells us is that the above values are in a List
of two tuples
, holding values of Char
type.
Let’s try to vary the number of Chars in the second tuple:
[ ( 'a', 'b' ), ( 'c' ) ]
This will throw a Type Mismatch
error. Indeed, for two tuples to be considered to be of the same type, they have to hold the same number of values, and those values also need to be of the same type.
The maximum number of values a tuple can hold in Elm is 9. If you try to add 10 or more values to a tuple, Elm will throw an error. Let’s try this out:
('1','2','3','4','5','6','7','8','9','0')
Here’s the error that comes back from the REPL:
---- Elm 0.19.0 ----------------------------------------------------------------
Read <https://elm-lang.org/0.19.0/repl> to learn more: exit, help, imports, etc.
--------------------------------------------------------------------------------
> ('1','2','3','4','5','6','7','8','9','0')
-- BAD TUPLE --------------------------------------------------------------- elm
I only accept tuples with two or three items. This has too many:
4| ('1','2','3','4','5','6','7','8','9','0')
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
I recommend switching to records. Each item will be named, and you can use the
`point.x` syntax to access them.
Note: Read <https://elm-lang.org/0.19.0/tuples> for more comprehensive advice on
working with large chunks of data in Elm.
Records
Records in Elm use curly brackets, and a label for each value must be provided. Records can also hold multiple values, and these values types’ don’t have to match. For example:
{ color="blue", quantity=17 }
In REPL, we get back:
{ color = "blue", quantity = 17 } : { color : String, quantity : number }
We will use records a lot in our Elm programs, as records allow us to model the data in a wide variety of scenarios.
An example where we already used a record in a previous article:
main = Browser.sandbox
{ init = 5
, update = update
, view = view
}
In the above code, the Browser.sandbox
function takes a Record
as its single parameter.
To test out the code in the REPL, we can just pass it the record itself:
{ init = 5, view = view, update = update }
Here’s the error that REPL throws:
I cannot find a `update` variable:
4| { init = 5, view = view, update = update }
^^^^^^
These names seem close though:
negate
truncate
abs
always
Hint: Read <https://elm-lang.org/0.19.0/imports> to see how `import`
declarations work in Elm.
-- NAMING ERROR ------------------------------------------------------------ elm
I cannot find a `view` variable:
4| { init = 5, view = view, update = update }
^^^^
These names seem close though:
e
min
pi
sin
Hint: Read <https://elm-lang.org/0.19.0/imports> to see how `import`
declarations work in Elm.
The error shows since there are no varables of update
or view
available.
So let’s add it and try again:
> view = "view info"
"view info" : String
> update = "update info"
"update info" : String
> { init = 5, view = view, update = update }
{ init = 5, update = "update info", view = "view info" }
: { init : number, view : String, update : String }
We have assigned values of type String
to the view
and update
variables in the Elm REPL. Then we entered the record, and the REPL returned types for each of the variables used in the record. Thus, in the preceding example, the model
is of type number
, the update
is of type String
, and the view
is also of type String
.
Sets
Sets are collections of unique values. Their uniqueness is guaranteed by the Elm programming language. We can instantiate sets as empty sets or use the fromList
function. Creating an empty set is easy: set = Set.empty
.
Let’s look at the other way of creating sets in Elm, by pointing our browser to an Ellie app example.
module Main exposing (main)
import Html exposing (Html, text)
import Set
set = Set.fromList [1,1,1,2]
main : Html msg
main =
text (Debug.toString set)
What we did in the preceding code was, after importing Set (to the variable we named set), we assigned the returned value from the evaluated expression: Set.fromList [1,1,1,2]
.
Next, we gave the set
variable to our main
function, to render it out as a text node. Of course, before it could be rendered out, we had to convert it to a String
. After pressing the Compile button in the Ellie-app, we should see the following result: Set.fromList [1,2]
.
Sets are useful when we are trying to find differences between data structures. Next, we’ll look at arrays.
Arrays
Arrays in Elm are zero-based, just like they are in JavaScript. With arrays, we can work with elements based on their index. Like sets, arrays can be created using the fromList
function.
Alternatively, we can create an empty array like this: array = Array.empty
. Let’s look at another Ellie-app example to test out arrays:
module Main exposing (main)
import Html exposing (Html, text)
import Array
array = Array.fromList [1,1,1,2]
array2 = Array.get 0 array
main: Html msg
main =
text ((Debug.toString array) ++ " " ++ (Debug.toString array2))
In the preceding code, we have a slight twist—we grouped the concatenation of two arrays and a space, all converted to Strings
, and then ran the text
function on them, finally passing the value returned from the evaluation of the expression to the main function.
The compiled code will display the following result: Array.fromList [1,1,1,2] Just 1
. For now, let’s just ignore what this Just result means, and let’s continue by looking at dictionaries in Elm.
Dictionaires
Dictionaries are also created using the fromList
function. Let’s open a brand new Ellie-app, and this time let’s add this code:
module Main exposing (main)
import Html exposing (Html, text)
import Dict
dict =
Dict.fromList
[ ("keyOne", "valueOne")
, ("keyTwo", "valueTwo")
]
main : Html msg
main =
text (Debug.toString dict)
The result of the above code is:
Dict.fromList [("keyOne","valueOne"),("keyTwo","valueTwo")]
Dict
is the data structure used to store pairs of keys and values. Keys must be unique. To learn more about this data structure, visit the official Elm lang docs.
Functions, if expressions, and types
Let’s create a new function in Elm REPL. We’ll call our function multiplyBy5:
multiplyBy5 num = 5 * num
The REPL will return this:
<function> : number -> number
The preceding line says that our multiplyBy5
function has the type of number -> number
. Let’s see what type will get returned from a function that works with Strings:
appendSuffix n = n ++ "ing"
As we already know, the ++
operator is the concat operator in Elm; it will join two Strings
together. Thus, expectedly, Elm REPL will return:
<function> : String -> String
As we can see, the preceding function is of type String -> String
.
But, what is this String -> String
? And, along the same lines, what is the Int -> Int
from the previous example? String -> String
simply means that the function expects a String
as its argument, and will also return a String
. For the Int -> Int
example, the function expects a value of type Int
and will also return a value of type Int
.
It’s time to take a look at the basics of types in if expressions in Elm. Consider the following snippet of code and the response REPL gave it:
> time = 24
24 : number
> if time < 12 then "morning" else "afternoon"
"afternoon" : String
In the preceding code, we are running an if
expression using the variable time
(which we assign the value of 24
). Then, we are running our comparison. Note that if expressions should actually be referred to as if-else
expressions, as if expressions must have an else, otherwise they won’t work in Elm. Both the if
and the else
branch must be of the same type. That’s why in the preceding example we are making sure that either result we get is of type String
.
In the next article, we’ll look at working with an improved Fruit to eat app.