Elm #
Building a New Application #
Using yarn and the parcel web application bundler
- Create new project
mkdir project && cd project
yarn add parcel-bundler elm
yarn run elm init # Initialise Elm application (creates elm.json)
yarn run elm install elm/http # Install additional Elm packages (optional)
- Create a very basic Elm application in
src/Main.elm
, e.g.:
import Html exposing (..)
main = text "Hello world"
- Create a
index.html
in the root directory where the Elm code will be embedded:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title></title>
</head>
<body>
<div id="main"></div>
<script src="./index.js"></script>
</body>
</html>
- Create a
index.js
to Elm and Parcel glue together:
import { Elm } from './src/Main.elm'
Elm.Main.init({
node: document.getElementById('main'),
});
- Run parcel
yarn run parcel index.html # Start development server
yarn run parcel watch index.thml # Start development server with automatic reloading
yarn run parcel build index.html # Build application (per default in dist/ directory, change with option -d
Data types #
Data types can normally be inferred, but it is recommended to add a type annotation to at least root-level variables / functions. Annotations are written a line above the variable / function definition, e.g.:
hello : String
hello = "Hello world!"
String
- Concatenation:
++
- Concatenation:
number
:Int
,Float
- Booleans:
True
,False
- Functions:
- Named functions:
isNegative n = n < 0
- Anonymous functions:
(\n -> n < 0) 4
(the parentheses are used here to define the boundaries of the function, so that4
is not considered part of it) - Functions are always curried, so a function
add a b = a + b
has the typenumber -> number -> number
- Named functions:
- Lists:
names = [ "Alice", "Bob", "Chuck" ]
- Several methods, e.g.:
List.length names
;List.map double numbers
(double is a function:double n = n * 2
)
- Several methods, e.g.:
- Records:
- Definition:
point = { x = 3, y = 4 }
- Access:
point.x
or.x point
- Destructuring:
under70 {age} = age < 70
(the functionunder70
takes a record with a fieldadd
, otherwise an error is thrown) - Update records:
{ point | x = 2 }
- Records vs. objects:
- You cannot ask for a field that does not exist.
- No field will ever be
undefined
ornull
. - You cannot create recursive records with a
this
orself
keyword.
- Definition:
- Tuples:
(True, "astring", 2)
Type Variables #
Type variables serve as placeholders for any type. They must always begin with a lowercase letter. E.g. List a
Constrained Type Variables #
Normally type variables like a
, b
can get filled with anything, but there are a few constrained type variables:
number
permitsInt
andFloat
appendable
permitsString
andList a
comparable
permitsInt
,Float
,Char
,String
, and lists/tuples ofcomparable
valuescompappend
permitsString
andList comparable
These constrained type variables exist to make operators like (+)
and (<)
a bit more flexible.
Type alias #
Types can be aliased, e.g.
type alias User =
{ name : String
, bio : String
}
Record conststructors #
Along with the creation of a type alias specifically for a record goes the creation of a record constructor. This makes it easier to build a record of the respective type. In the REPL this could look like this:
> type alias User = { name : String, bio : String }
> User "Tom" "Friendly Carpenter"
{ name = "Tom", bio = "Friendly Carpenter" }
Custom types$ #
Custom types are types which consists of several *variants *(thus they are ADTs of the sum type, unlike records and tuples, which are of the product type). The variants can have associated data, which can have any type and consist of multiple types. A more complex example of a custom type user with three variants:
type User
= Regular {user: String, password: String } Int Location
| Visitor String
| Anonymous
Custom types are very important for messages, for what customarily a Msg
type is used.
Expressions #
if - else if - else #
if String.length "test" > 4 then
"too long!"
else if String.length "test" < 2 then
"too short!"
else
"fits exactly!"
Pattern matching #
Given a custom type:
type User
= Regular String Int
| Visitor String
We can use pattern matching function to destructure it:
toName : User -> String
toName user =
case user of
-- Use _ wildcard to discard unused values
Regular name _ ->
name
Visitor name ->
name
Pipelines #
Pipelines can replace expressions like
sanitize : String -> Maybe Int
sanitize input =
String.toInt (String.trim input)
with
sanitize : String -> Maybe Int
sanitize input =
input
|> String.trim
|> String.toInt
Error Handling #
For error handling two predefined types are used, Result
and Maybe
Maybe #
type Maybe a
= Just a
| Nothing
Mainly used for partial functions (e.g. String.toFloat
) and optional fields in records (but avoid overuse!)
Result #
type Result error value
= Ok value
| Err error
Mainly used for error reporting and error recovery (where a different course of action can be taken depending on the kind of error)
Architecture #
The logic of every Elm program will break up into three cleanly separated parts:
- Model: the state of your application
- Update: a way to update your state
- View: a way to view your state as HTML
This gives the following skeleton for every application:
import Html exposing (..)
-- MODEL
type alias Model = { ... }
-- UPDATE
type Msg = Reset | ...
update : Msg -> Model -> Model
update msg model =
case msg of
Reset -> ...
...
-- VIEW
view : Model -> Html Msg
view model =
...