Elm

Elm #

Building a New Application #

Using yarn and the parcel web application bundler

  1. 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)
  1. Create a very basic Elm application in src/Main.elm, e.g.:
import Html exposing (..)
	
main = text "Hello world"
  1. 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>
  1. Create a index.js to Elm and Parcel glue together:
import { Elm } from './src/Main.elm'
	
Elm.Main.init({
	node: document.getElementById('main'),
});
  1. 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: ++
  • 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 that 4 is not considered part of it)
    • Functions are always curried, so a function add a b = a + b has the type number -> number -> number
  • Lists: names = [ "Alice", "Bob", "Chuck" ]
    • Several methods, e.g.: List.length names; List.map double numbers (double is a function: double n = n * 2)
  • Records:
    • Definition: point = { x = 3, y = 4 }
    • Access: point.x or .x point
    • Destructuring: under70 {age} = age < 70 (the function under70 takes a record with a field add, 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 or null.
      • You cannot create recursive records with a this or self keyword.
  • 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 permits Int and Float
  • appendable permits String and List a
  • comparable permits Int, Float, Char, String, and lists/tuples of comparable values
  • compappend permits String and List 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 =
...

Resources #

General #

Talks #

Examples #