Getting started
Introduction
Elm is a purely functional language that compiles to JavaScript. It guarantees no runtime exceptions and provides a delightful development experience with helpful compiler messages.
- Version: Elm 0.19.1 (current)
- Official Site: elm-lang.org
- Package Registry: package.elm-lang.org
Installation
# Via npm
npm install -g elm
# Via Homebrew (macOS)
brew install elm
# Verify installation
elm --version
Quick Start
# Initialize project
elm init
# Compile to JavaScript
elm make src/Main.elm --output=main.js
# Compile with optimizations
elm make src/Main.elm --optimize --output=main.js
# Start development server
elm reactor
# Live reload development
elm-live src/Main.elm --open --output=main.js
Hello World
module Main exposing (main)
import Html exposing (text)
main =
text "Hello, World!"
The Elm Architecture
Model-Update-View Pattern
The core pattern for all Elm applications.
-- MODEL
type alias Model =
{ count : Int
, message : String
}
init : Model
init =
{ count = 0
, message = "Click the button!"
}
-- UPDATE
type Msg
= Increment
| Decrement
| Reset
update : Msg -> Model -> Model
update msg model =
case msg of
Increment ->
{ model | count = model.count + 1 }
Decrement ->
{ model | count = model.count - 1 }
Reset ->
{ model | count = 0 }
-- VIEW
view : Model -> Html Msg
view model =
div []
[ button [ onClick Decrement ] [ text "-" ]
, span [] [ text (String.fromInt model.count) ]
, button [ onClick Increment ] [ text "+" ]
, button [ onClick Reset ] [ text "Reset" ]
]
With Commands
For side effects (HTTP, random, etc.)
import Http
import Json.Decode as Decode
type Msg
= GotData (Result Http.Error String)
| FetchData
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
FetchData ->
( { model | loading = True }
, Http.get
{ url = "https://api.example.com/data"
, expect = Http.expectString GotData
}
)
GotData result ->
case result of
Ok data ->
( { model | data = data, loading = False }
, Cmd.none
)
Err _ ->
( { model | error = "Failed", loading = False }
, Cmd.none
)
With Subscriptions
For listening to external events.
import Browser.Events
import Time
type Msg
= Tick Time.Posix
| KeyPressed String
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.batch
[ Time.every 1000 Tick
, Browser.Events.onKeyPress keyDecoder
]
keyDecoder : Decode.Decoder Msg
keyDecoder =
Decode.map KeyPressed
(Decode.field "key" Decode.string)
Types
Basic Types
-- Primitives
number : Int
number = 42
decimal : Float
decimal = 3.14
text : String
text = "Hello"
flag : Bool
flag = True
-- Lists (homogeneous)
numbers : List Int
numbers = [1, 2, 3, 4]
-- Tuples (heterogeneous)
pair : (String, Int)
pair = ("Alice", 25)
triple : (Int, String, Bool)
triple = (1, "two", True)
Type Aliases
-- Record type alias
type alias User =
{ name : String
, age : Int
, email : String
}
-- Usage
user : User
user =
{ name = "Alice"
, age = 30
, email = "alice@example.com"
}
-- Update syntax
olderUser : User
olderUser =
{ user | age = 31 }
Custom Types (Union Types)
-- Simple enum
type Status
= Loading
| Success
| Failure
-- With associated data
type User
= Anonymous
| Registered String Int
| Admin String String
-- Recursive types
type Tree a
= Empty
| Node a (Tree a) (Tree a)
-- Generic Result-like type
type Response a
= Loading
| Success a
| Error String
Maybe Type
For values that might not exist.
-- Definition
type Maybe a
= Just a
| Nothing
-- Usage
findUser : Int -> Maybe User
findUser id =
if id > 0 then
Just { name = "Alice", age = 30 }
else
Nothing
-- Pattern matching
displayAge : Maybe Int -> String
displayAge maybeAge =
case maybeAge of
Just age ->
"Age: " ++ String.fromInt age
Nothing ->
"Age unknown"
-- Using Maybe.map
incrementAge : Maybe Int -> Maybe Int
incrementAge =
Maybe.map (\age -> age + 1)
-- Maybe.withDefault
getAge : Maybe Int -> Int
getAge maybeAge =
Maybe.withDefault 0 maybeAge
Result Type
For operations that can fail.
-- Definition
type Result error value
= Ok value
| Err error
-- Usage
divide : Float -> Float -> Result String Float
divide a b =
if b == 0 then
Err "Cannot divide by zero"
else
Ok (a / b)
-- Pattern matching
displayResult : Result String Int -> String
displayResult result =
case result of
Ok value ->
"Success: " ++ String.fromInt value
Err error ->
"Error: " ++ error
-- Chaining with Result.andThen
parseAndValidate : String -> Result String Int
parseAndValidate input =
String.toInt input
|> Result.fromMaybe "Not a number"
|> Result.andThen validatePositive
validatePositive : Int -> Result String Int
validatePositive n =
if n > 0 then
Ok n
else
Err "Must be positive"
Pattern Matching
Case Expressions
-- Basic pattern matching
describe : Int -> String
describe n =
case n of
0 ->
"zero"
1 ->
"one"
_ ->
"many"
-- Matching custom types
statusMessage : Status -> String
statusMessage status =
case status of
Loading ->
"Loading..."
Success ->
"Done!"
Failure ->
"Error occurred"
Destructuring
-- Lists
first : List a -> Maybe a
first list =
case list of
[] ->
Nothing
head :: tail ->
Just head
-- Tuples
addPair : (Int, Int) -> Int
addPair pair =
case pair of
(x, y) ->
x + y
-- Records
getName : User -> String
getName user =
case user of
{ name } ->
name
-- Alternative record syntax
getEmail : User -> String
getEmail { email } =
email
Nested Patterns
-- Complex pattern matching
describe : Maybe (List Int) -> String
describe value =
case value of
Nothing ->
"No list"
Just [] ->
"Empty list"
Just [x] ->
"One item: " ++ String.fromInt x
Just (x :: y :: rest) ->
"Multiple items starting with "
++ String.fromInt x
-- Using as pattern
transform : Maybe User -> String
transform maybeUser =
case maybeUser of
Just ({ name, age } as user) ->
name ++ " is " ++ String.fromInt age
Nothing ->
"No user"
Functions
Function Syntax
-- Simple function
add : Int -> Int -> Int
add x y =
x + y
-- Anonymous function
increment : Int -> Int
increment =
\n -> n + 1
-- Partial application
add5 : Int -> Int
add5 =
add 5
-- Multiple arguments
greet : String -> String -> String
greet greeting name =
greeting ++ ", " ++ name ++ "!"
Pipeline Operator
-- Forward pipe (|>)
result : Int
result =
[1, 2, 3, 4]
|> List.map (\x -> x * 2)
|> List.filter (\x -> x > 4)
|> List.sum
-- Result: 14
-- Backward pipe (<|)
result : String
result =
String.toUpper <|
String.trim <|
" hello "
Composition
-- Function composition (>>)
processString : String -> String
processString =
String.trim
>> String.toLower
>> String.reverse
-- Backward composition (<<)
validate : String -> Result String Int
validate =
String.toInt
<< String.trim
Let Expressions
distance : Float -> Float -> Float -> Float
distance x1 y1 x2 y2 =
let
dx =
x2 - x1
dy =
y2 - y1
squaredDistance =
dx * dx + dy * dy
in
sqrt squaredDistance
HTML and Views
Basic HTML
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
-- Simple elements
view : Html msg
view =
div []
[ h1 [] [ text "Title" ]
, p [] [ text "Paragraph" ]
, span [] [ text "Inline" ]
]
-- With attributes
styledView : Html msg
styledView =
div [ class "container", id "main" ]
[ h1 [ class "title" ] [ text "Hello" ]
, p [ style "color" "blue" ] [ text "Text" ]
]
Event Handlers
-- Click events
button : Html Msg
button =
Html.button
[ onClick Increment ]
[ text "Click me" ]
-- Input events
inputField : Html Msg
inputField =
input
[ type_ "text"
, placeholder "Enter text"
, value model.inputValue
, onInput UpdateInput
]
[]
-- Custom event decoder
onEnter : msg -> Attribute msg
onEnter msg =
on "keypress" <|
Decode.andThen
(\key ->
if key == 13 then
Decode.succeed msg
else
Decode.fail "Not Enter"
)
keyCode
Lists and Conditionals
-- Rendering lists
viewUsers : List User -> Html msg
viewUsers users =
ul [] (List.map viewUser users)
viewUser : User -> Html msg
viewUser user =
li [] [ text user.name ]
-- Conditional rendering
viewStatus : Bool -> Html msg
viewStatus isLoading =
if isLoading then
div [] [ text "Loading..." ]
else
div [] [ text "Ready!" ]
-- Using case for multiple conditions
viewState : Status -> Html msg
viewState status =
case status of
Loading ->
spinner
Success ->
successMessage
Failure ->
errorMessage
JSON Decoders
Basic Decoders
import Json.Decode as Decode exposing (Decoder)
-- Primitive decoders
stringDecoder : Decoder String
stringDecoder =
Decode.string
intDecoder : Decoder Int
intDecoder =
Decode.int
boolDecoder : Decoder Bool
boolDecoder =
Decode.bool
-- Field decoder
nameDecoder : Decoder String
nameDecoder =
Decode.field "name" Decode.string
Object Decoders
-- Simple record decoder
type alias User =
{ name : String
, age : Int
, email : String
}
userDecoder : Decoder User
userDecoder =
Decode.map3 User
(Decode.field "name" Decode.string)
(Decode.field "age" Decode.int)
(Decode.field "email" Decode.string)
-- Alternative with pipeline style
import Json.Decode.Pipeline exposing (required, optional)
userDecoderPipeline : Decoder User
userDecoderPipeline =
Decode.succeed User
|> required "name" Decode.string
|> required "age" Decode.int
|> optional "email" Decode.string "no-email"
Complex Decoders
-- List decoder
usersDecoder : Decoder (List User)
usersDecoder =
Decode.list userDecoder
-- Nested objects
type alias Post =
{ title : String
, author : User
, tags : List String
}
postDecoder : Decoder Post
postDecoder =
Decode.map3 Post
(Decode.field "title" Decode.string)
(Decode.field "author" userDecoder)
(Decode.field "tags" (Decode.list Decode.string))
-- Optional fields
type alias Profile =
{ name : String
, bio : Maybe String
}
profileDecoder : Decoder Profile
profileDecoder =
Decode.map2 Profile
(Decode.field "name" Decode.string)
(Decode.maybe (Decode.field "bio" Decode.string))
-- Custom decoders
statusDecoder : Decoder Status
statusDecoder =
Decode.string
|> Decode.andThen
(\str ->
case str of
"loading" ->
Decode.succeed Loading
"success" ->
Decode.succeed Success
"failure" ->
Decode.succeed Failure
_ ->
Decode.fail "Unknown status"
)
Using Decoders
-- Decoding JSON strings
parseUser : String -> Result Decode.Error User
parseUser jsonString =
Decode.decodeString userDecoder jsonString
-- With HTTP
fetchUser : Cmd Msg
fetchUser =
Http.get
{ url = "https://api.example.com/user"
, expect = Http.expectJson GotUser userDecoder
}
type Msg
= GotUser (Result Http.Error User)
Commands and HTTP
HTTP Requests
import Http
import Json.Decode as Decode
-- GET request
fetchData : Cmd Msg
fetchData =
Http.get
{ url = "https://api.example.com/data"
, expect = Http.expectJson GotData dataDecoder
}
-- POST request
postData : User -> Cmd Msg
postData user =
Http.post
{ url = "https://api.example.com/users"
, body = Http.jsonBody (encodeUser user)
, expect = Http.expectJson UserCreated userDecoder
}
-- Custom request
updateUser : Int -> User -> Cmd Msg
updateUser id user =
Http.request
{ method = "PUT"
, headers = [ Http.header "Authorization" "Bearer token" ]
, url = "https://api.example.com/users/" ++ String.fromInt id
, body = Http.jsonBody (encodeUser user)
, expect = Http.expectJson UserUpdated userDecoder
, timeout = Nothing
, tracker = Nothing
}
Command Batching
-- Batch multiple commands
init : (Model, Cmd Msg)
init =
( initialModel
, Cmd.batch
[ fetchUsers
, fetchPosts
, fetchComments
]
)
-- No command
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
SomeMsg ->
( newModel, Cmd.none )
Random Values
import Random
-- Generate random integer
generateDiceRoll : Cmd Msg
generateDiceRoll =
Random.generate GotDiceRoll (Random.int 1 6)
type Msg
= GotDiceRoll Int
-- Generate multiple random values
type alias Point =
{ x : Float, y : Float }
randomPoint : Random.Generator Point
randomPoint =
Random.map2 Point
(Random.float 0 100)
(Random.float 0 100)
generatePoint : Cmd Msg
generatePoint =
Random.generate GotPoint randomPoint
Lists and Common Functions
List Operations
-- Creating lists
list1 : List Int
list1 = [1, 2, 3]
list2 : List Int
list2 = 1 :: 2 :: 3 :: []
range : List Int
range = List.range 1 10
-- Common functions
List.length [1, 2, 3] -- 3
List.isEmpty [] -- True
List.head [1, 2, 3] -- Just 1
List.tail [1, 2, 3] -- Just [2, 3]
List.take 2 [1, 2, 3, 4] -- [1, 2]
List.drop 2 [1, 2, 3, 4] -- [3, 4]
List.reverse [1, 2, 3] -- [3, 2, 1]
List.member 2 [1, 2, 3] -- True
List Transformations
-- Map
List.map (\x -> x * 2) [1, 2, 3]
-- [2, 4, 6]
-- Filter
List.filter (\x -> x > 2) [1, 2, 3, 4]
-- [3, 4]
-- FilterMap
List.filterMap String.toInt ["1", "a", "3"]
-- [1, 3]
-- Fold (reduce)
List.foldl (+) 0 [1, 2, 3, 4]
-- 10
List.foldr (::) [] [1, 2, 3]
-- [1, 2, 3]
-- Concat
List.concat [[1, 2], [3, 4]]
-- [1, 2, 3, 4]
-- ConcatMap (flatMap)
List.concatMap (\x -> [x, x * 10]) [1, 2, 3]
-- [1, 10, 2, 20, 3, 30]
String Operations
-- Common functions
String.length "hello" -- 5
String.isEmpty "" -- True
String.reverse "hello" -- "olleh"
String.toUpper "hello" -- "HELLO"
String.toLower "HELLO" -- "hello"
String.trim " hello " -- "hello"
-- Combining
String.append "Hello" " World" -- "Hello World"
String.concat ["a", "b", "c"] -- "abc"
String.join ", " ["a", "b"] -- "a, b"
-- Splitting
String.split "," "a,b,c" -- ["a", "b", "c"]
String.words "hello world" -- ["hello", "world"]
String.lines "a\nb\nc" -- ["a", "b", "c"]
-- Checking
String.startsWith "He" "Hello" -- True
String.endsWith "lo" "Hello" -- True
String.contains "ell" "Hello" -- True
-- Conversion
String.fromInt 42 -- "42"
String.toInt "42" -- Just 42
String.fromFloat 3.14 -- "3.14"
String.toFloat "3.14" -- Just 3.14
Modules and Imports
Module Definition
-- Exposing specific items
module Main exposing (main, Model, Msg)
-- Exposing everything (not recommended)
module Utils exposing (..)
-- Exposing types with constructors
module Types exposing (User(..), Status(..))
-- Exposing types without constructors
module Types exposing (User, Status)
Importing Modules
-- Basic import
import Html
-- Exposing specific items
import Html exposing (div, text)
-- Exposing all
import Html.Attributes exposing (..)
-- Qualified import
import Json.Decode as Decode
import Json.Encode as Encode
-- Multiple imports
import Html exposing (Html, div)
import Html.Attributes exposing (class, id)
import Html.Events exposing (onClick)
Module Organization
-- src/Main.elm
module Main exposing (main)
import Browser
import Model exposing (Model, init)
import Update exposing (Msg, update)
import View exposing (view)
main : Program () Model Msg
main =
Browser.sandbox
{ init = init
, update = update
, view = view
}
-- src/Model.elm
module Model exposing (Model, init)
-- src/Update.elm
module Update exposing (Msg(..), update)
-- src/View.elm
module View exposing (view)
Compilation and Development
Compilation Commands
# Compile single file to JavaScript
elm make src/Main.elm --output=main.js
# Compile with optimizations
elm make src/Main.elm --optimize --output=main.js
# Compile to HTML (includes runtime)
elm make src/Main.elm --output=index.html
# Debug mode (default)
elm make src/Main.elm --debug --output=main.js
Development Server
# Start Elm Reactor (localhost:8000)
elm reactor
# Navigate to:
# http://localhost:8000/src/Main.elm
Live Reload Development
# Install elm-live
npm install -g elm-live
# Start with live reload
elm-live src/Main.elm --open --output=main.js
# With custom port
elm-live src/Main.elm --port=8080 --output=main.js
# With hot reloading
elm-live src/Main.elm --hot --output=main.js
Project Management
# Initialize new project
elm init
# Install package
elm install elm/http
elm install elm/json
# Update dependencies
elm bump
# Validate elm.json
elm make src/Main.elm --output=/dev/null
elm.json Structure
{
"type": "application",
"source-directories": ["src"],
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"elm/browser": "1.0.2",
"elm/core": "1.0.5",
"elm/html": "1.0.0",
"elm/http": "2.0.0",
"elm/json": "1.1.3"
},
"indirect": {
"elm/bytes": "1.0.8",
"elm/file": "1.0.5",
"elm/time": "1.0.0",
"elm/url": "1.0.0",
"elm/virtual-dom": "1.0.3"
}
},
"test-dependencies": {
"direct": {},
"indirect": {}
}
}
Program Types
Browser.sandbox
For simple apps without side effects.
import Browser
main : Program () Model Msg
main =
Browser.sandbox
{ init = init
, update = update
, view = view
}
-- No Cmd or Sub
update : Msg -> Model -> Model
Browser.element
For apps with commands/subscriptions.
import Browser
main : Program () Model Msg
main =
Browser.element
{ init = init
, update = update
, view = view
, subscriptions = subscriptions
}
init : (Model, Cmd Msg)
update : Msg -> Model -> (Model, Cmd Msg)
view : Model -> Html Msg
subscriptions : Model -> Sub Msg
Browser.document
For apps controlling <head> and <body>.
import Browser
main : Program () Model Msg
main =
Browser.document
{ init = init
, update = update
, view = view
, subscriptions = subscriptions
}
view : Model -> Browser.Document Msg
view model =
{ title = "Page Title"
, body =
[ div [] [ text "Content" ]
]
}
Browser.application
For single-page apps with URL routing.
import Browser
import Browser.Navigation as Nav
import Url exposing (Url)
main : Program () Model Msg
main =
Browser.application
{ init = init
, update = update
, view = view
, subscriptions = subscriptions
, onUrlRequest = LinkClicked
, onUrlChange = UrlChanged
}
type Msg
= LinkClicked Browser.UrlRequest
| UrlChanged Url
init : () -> Url -> Nav.Key -> (Model, Cmd Msg)
Common Gotchas
Type Annotations Required
-- Error: missing type annotation
add x y =
x + y
-- Fixed
add : Int -> Int -> Int
add x y =
x + y
Record Update Syntax
-- Error: cannot reassign fields
user.age = 31
-- Fixed: use update syntax
{ user | age = 31 }
If/Then/Else Required
-- Error: missing else branch
if x > 0 then
"positive"
-- Fixed: else is required
if x > 0 then
"positive"
else
"not positive"
Comparing Functions
-- Error: cannot compare functions with ==
(\x -> x + 1) == (\x -> x + 1)
-- Use named functions or refactor logic
String Concatenation
-- Error: cannot use + for strings
"Hello" + " World"
-- Fixed: use ++
"Hello" ++ " World"
-- Or use String.concat
String.concat ["Hello", " ", "World"]
Partial Application in Pipelines
-- Error: wrong argument order
users
|> List.filter isActive
-- Fixed: lambda wrapper
users
|> List.filter (\user -> isActive user)
-- Or use function composition
users
|> List.filter isActive
Importing Constructors
-- Won't compile: Maybe not in scope
case value of
Just x -> ...
Nothing -> ...
-- Fixed: import with constructors
import Maybe exposing (Maybe(..))
-- Or use qualified names
case value of
Maybe.Just x -> ...
Maybe.Nothing -> ...
Best Practices
Type Safety First
Always use explicit type annotations for top-level functions. Let the compiler catch errors early.
-- Good
getUserName : User -> String
getUserName user =
user.name
-- Avoid (inference works but less clear)
getUserName user =
user.name
Small Update Functions
Break large update functions into smaller helpers.
-- Good
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
UserMsg userMsg ->
updateUser userMsg model
PostMsg postMsg ->
updatePost postMsg model
-- Avoid giant case expressions
Use Custom Types
Prefer custom types over strings/booleans for state.
-- Good
type Status = Loading | Success | Failure
-- Avoid
type alias Model = { status : String }
Decode with Pipelines
Use Json.Decode.Pipeline for cleaner decoders.
import Json.Decode.Pipeline as Pipeline
userDecoder : Decoder User
userDecoder =
Decode.succeed User
|> Pipeline.required "name" Decode.string
|> Pipeline.required "age" Decode.int
|> Pipeline.optional "email" Decode.string ""
Also see
- Official Elm Guide - Comprehensive introduction to Elm
- Elm Package Registry - Search packages and documentation
- Elm Syntax Reference - Language syntax overview
- Elm in Action - In-depth book by Richard Feldman
- Elm Slack - Active community chat
- Elm Discourse - Forum for questions and discussions
- Elm Radio Podcast - Weekly Elm podcast
- Beginning Elm - Free online book for beginners