Elm - FP Web Dev
What is Elm?
Elm is a functional programming language used to build web applications which transpiles to JavaScript.
Why Should You Care About Elm?
Elm provides significant advantages over JavaScript:
- Increased reliability: The Elm compiler catches common errors that happen in JavaScript like those caused by typos or type coertion, etc, at compile time. This translates in having no pernicious and hard to spot runtime exceptions, since these errors will prevent your code from transpiling.
- Great developer experience: The Elm compiler produces awesome error messages that help you
Basic Elm Syntax
Comments:
-- single line comment
{- block comment
that can span multiple lines -}
Variables are immutable and can only be assigned (once) at the moment they’re declared:
x = 1
helloWorld = "hello world"
longstring = """
multiline strings
"""
This is how you define a function on Elm:
add a b = a + b
This is equivalent to this code in JavaScript:
function add(a, b) {
return a + b
}
You can call this function as follows
> add 1 2
3
Here we have yet another function with an if/else
statement:
pluralize singular plural quantity =
if quantity == 1 then
singular
else
plural
Which is equivalent to the following JavaScript:
function pluralize(singular, plural, quantity) {
if (quantity === 1) return singular
else return plural
}
There’s a couple of differences though. The else
in Elm is required since the code above is evaluated as a single expression (like the ternary operator in JavaScript).
You can call this function in Elm as follows:
> pluralize "leaf" "leaves" 2
2 leaves
The parenthesis aren’t normally required to call functions but they are often used to disambiguate which arguments we’re passing to which functions.
Anonymous functions (arrow functions) look like this:
(\n -> n%2 /= 0)
-- in JavaScript
-- n => n%2 != 0
In addition to if/else
statements, Elm also has case expressions similar in behavior to JavaScript switch
statement (in reality they do pattern matching which we’ll explain later in detail):
case msg.operation of
"DELETE_TODO" ->
-- delete
"ADD_TODO" ->
-- add
"UPDATE_TODO" ->
-- update
_ ->
-- default branch
Unlike the switch
statement there’s no fall through between cases and therefore no need for the break
keyword.
let expressions allow you to define variables within a givin scope. This lets you write simpler expressions:
let
pi = 3.1416
radius = 20
in
2 * pi * radius
Elm Types and Type Annotations
Booleans:
True
False
-- equals
1 == 1
-- negation operator
not (1 == 1)
-- not equals
1 /= 2
Strings:
-- a simple string
"hello world"
-- string concatenation evaluates to "hello world"
"hello" ++ " world"
-- convert to string
toString 10 == "10"
Type Annotations
Elm allows you to provide type annotations for your variables:
helloWorld: String;
helloWord = "hello world"
stars: Int;
stars = 22;
searchResult: {name: String, stars: Int}
searchResult = {name = "Vintharas/myrepo", stars: 1}
list: List string
list = ["ha", "ha", "ho"]
The Elm compiler will check these type annotations and make sure that they are followed within your code. This gets more interesting when you can define your own types through type aliases:
type alias SearchResult =
{
id: Int,
name: String,
stars: Int
}
Which then you can reuse across your codebase:
model = {
query: String
,results: List SearchResult
}
A common type in Elm would represent a message within the Elm architecture:
type alias Msg = {
operation: String
, data: Int
}
We can also use type annotations to represent functions:
add: Int Int -> Int
-- takes two ints and returns yet another int
For instance, the Elm Architecture view could be seen as follows:
view: Model -> Html Msg
-- view is a function that takes a Model as argument
-- and returns an Html of type Msg (whose events trigger messages of type Msg)
-- this is essentially generics
You can also use type annotations to denote higher-order functions:
prepend prefix word = prefix ++ word
prepend:string -> string -> string
An to denote union types, types defined as the union of other types:
type Bool = True | False
type Order = Ascending | Descending
-- Union types can also include generic types
type ColumnOrder = Ascending String
| Descending String
-- Which gets useful to describe messages in the
-- Elm architecture
type Msg = DeleteById Int
| Update Int
| Search String
Elm Data Structures/Collections. Records, Tuples and Lists
Records
A record is hash map or dictionary that is immutable. You define it like this:
record =
{ name = "rusty iron sword", weight = 10, length = 1.2 }
-- you can access its properties using dot notation
record.name == "rust iron sword"
Records support a similar feature to Object spread operator in JavaScript to update an existing record with new information (which results in a new record):
-- creating a new record with all the properties from sword and a new name "Krull"
newSword = {sword | name = "shiny iron sword"}
Tuples
A tuple is a collection of unnamed items that can have any type and is immutable:
tuple = (1, "and", 2)
-- you can use tuple destructuring to access the elements within a tuple
(first, and, second) = (1, "and", 2)
first == 1
Lists
A list is a collection of items with the same type that is immutable (like an immutable array):
[1, 2, 3 ]
Elm lists have interesting functions to work with collections. For instance a filter
function:
isOdd n = n % 2 /= 0
List.filter isOdd [1, 2, 3, 4, 5] == [1, 3, 5]
-- this is equivalent to
List.filter (\n -> n % 2 /= 0) [1, 2, 3, 4, 5] == [1, 3, 5]
And the map
function:
List.map (\n -> n*3) [1, 1, 1] == [3, 3, 3]
All these operations have no side effects, that is, the don’t change the original list but create a new one with the desired changes.
Elm Special Features
Partial Application
All functions within Elm are curried which allows us to use partial application to our hearts content:
add a b =
a + b
add 1 2 == 3
-- an add5 function resulting of partially applying the add function
add5 =
add 5
add5 1 == 6
You can clearly see this in the elm REPL which prints the type of any function that you describe. For instance, typing this function in the REPL:
> add a b = a + b
Will evaluate as this:
<function> : number -> number -> number
Which shows a curried funtion (the original took two integer arguments but the new function takes only one and returns a function that takes another integer).
The pipeline operator
Elm uses functions heavily. Functions that call other functions which in turn call other functions ad infinitum. This results in a code similar to this with subsequent calls wrapped in parenthesis:
List.reverse (List.filter (\n -> n < 4) [1, 2, 3, 4, 5])
A more succint and readable way to write an equivalent makes use of the pipeline operator:
[1, 2, 3, 4, 5]
|> List.filter (\n -> n < 4)
|> List.reverse
where the pipeline operator |>
subsequently calls each function which the result of a previous function.
Pattern Matching
- TODO: complete this section
The Elm Architecture
The Elm architecture is the way that you build web applications in Elm. It’s based on three pillars:
- Model: The state of your application
- View: A way to view the state of your application as HTML
- Update: A way to update the way of your application
This establishes a cycle where:
user interaction -> message (command) -> Update (state) -> model -> View -> HTML (the user can interact with)
The Elm Development Environment
- TODO: complete this section
The Elm Package Manager
- TODO: complete this section
Elm and Browser APIs
A Note on Error Handling
Elm doesn’t throw runtime exceptions. How does it then let you handle runtime errors? Instead of throwing exceptions Elm provides the Result
and Maybe
types.
The Result
type is a wrapper around an operation that can go well or result in an error:
-- pseudocode
type Result =
Ok allThingsGood |
Err somethingBadHappened
For instance, the String.Int
function lets you parse Strings
into integers. This operation can fail whenever we try to parse a string that doesn’t contain an integer:
String.Int "42" -- Ok 42
String.Int "Cucumber" -- Err "Cucumber is not an Int"
In order to be able to use the actual value within the Result
type we need to take into consideration the possibility of an error happening which guides you toward writing less error-prone applications. A special case of Result
is the Maybe
type reserved to represent when a function may not return a value:
-- pseudocode
type Maybe =
Just aValue |
Nothing
For instance, we can use List.head
to get the first element of a List. However, a list may be empty in which case we wouldn’t be able to get nothing. Instead of returning null
or undefined
like JavaScript could the List.head will return the Nothing
type:
List.head [1, 2, 3] -- Just 1
List.head [] -- Nothing
Elm, DOM and Virtual DOM
Elm provides a series of functions that allow you to model the browser DOM which result in a virtual representation of the DOM or VDOM. This is how it looks:
ul [ class "ingredients" ]
[ li [] [ text "100 grams sugar" ]
, li [] [ text "2 oz white chocolate" ]
]
Parsing JSON in Elm
- TODO: complete this section
For more information refer to Elm’s documentation about decoders.
Making AJAX requests in Elm
- TODO: complete this section
Debugging
The Debug.log
function is useful when debugging your code as it allows you to print arbitrary information into the browser console.log
. It returns the same value that you pass in so you can use it in the same spot as an actual value within your programs:
value = "cucumber"
Debug.log "a message:" value
-- prints a message: cucumber
References
Written by Jaime González García , dad, husband, software engineer, ux designer, amateur pixel artist, tinkerer and master of the arcane arts. You can also find him on Twitter jabbering about random stuff.