Foundational Elm development patterns covering The Elm Architecture (TEA), type-safe frontend development, and core language features. Use when building Elm applications, understanding functional patterns in web development, working with union types and Maybe/Result, or needing guidance on Elm tooling and ecosystem.
Installs into .claude/skills of the current project.
Are you the author of Lang Elm Dev?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/arustydev-lang-elm-dev)
---
name: lang-elm-dev
description: Foundational Elm development patterns covering The Elm Architecture (TEA), type-safe frontend development, and core language features. Use when building Elm applications, understanding functional patterns in web development, working with union types and Maybe/Result, or needing guidance on Elm tooling and ecosystem.
---
# Elm Development
Foundational patterns for building type-safe frontend applications with Elm. This skill covers The Elm Architecture, core language features, and Elm's unique approach to functional web development.
## Overview
Elm is a purely functional language for building reliable web applications with no runtime exceptions. It compiles to JavaScript and enforces immutability, pure functions, and explicit error handling through its type system.
**This skill covers:**
- The Elm Architecture (Model-View-Update pattern)
- Core syntax (types, functions, pattern matching)
- Type system (union types, type aliases, Maybe/Result)
- Working with JSON and HTTP
- Common patterns and idioms
- Elm tooling (elm-format, elm-review, elm-test)
**This skill does NOT cover:**
- Advanced UI patterns → See elm-ui or elm-css specific skills
- Complex state management → See elm-spa patterns
- Backend integration specifics → See framework-specific docs
- JavaScript interop deep-dives → See ports/flags documentation
---
## Quick Reference
| Task | Pattern |
|------|---------|
| Define type alias | `type alias User = { name : String, age : Int }` |
| Define union type | `type Msg = Increment \| Decrement` |
| Pattern match | `case msg of ...` |
| Handle Maybe | `Maybe.withDefault default maybeValue` |
| Handle Result | `Result.withDefault default result` |
| Update function | `update : Msg -> Model -> ( Model, Cmd Msg )` |
| View function | `view : Model -> Html Msg` |
| HTTP request | `Http.get { url = "...", expect = Http.expectJson ... }` |
| Decode JSON | `Decode.field "name" Decode.string` |
---
## Module System
Elm uses a simple but strict module system where every file is a module and must declare what it exposes.
### Module Declaration
```elm
-- Every file MUST start with a module declaration
module MyModule exposing (publicFunction, PublicType, PublicMsg(..))
-- Expose everything (not recommended for libraries)
module MyModule exposing (..)
-- Expose specific items
module MyModule exposing
( User -- Type but not constructors
, Msg(..) -- Type with all constructors
, init -- Function
, update -- Function
)
-- Port modules (for JavaScript interop)
port module Main exposing (..)
```
### Import Syntax
```elm
-- Basic import (must qualify: Dict.empty)
import Dict
-- Import with alias (shorter qualification)
import Json.Decode as Decode
import Json.Encode as E
-- Import exposing specific items (can use unqualified)
import Html exposing (Html, div, text, button)
import Html.Events exposing (onClick, onInput)
-- Import exposing everything (use sparingly)
import List exposing (..)
-- Combined: alias + exposing
import Html.Attributes as Attr exposing (class, id)
-- Can use: Attr.style or class (unqualified)
-- Import type constructors
import Maybe exposing (Maybe(..)) -- Exposes Just and Nothing
import Result exposing (Result(..)) -- Exposes Ok and Err
```
### Module Organization
```elm
-- Recommended project structure:
-- src/
-- Main.elm -- Entry point
-- Types.elm -- Shared types
-- Api.elm -- HTTP/API functions
-- Page/
-- Home.elm -- Page modules
-- Users.elm
-- Components/
-- Button.elm -- Reusable components
-- Modal.elm
-- Util/
-- Time.elm -- Helper functions
-- Types.elm - Shared across modules
module Types exposing (User, Msg(..), Route(..))
type alias User =
{ name : String
, email : String
}
type Msg
= NavigateTo Route
| GotUsers (Result Http.Error (List User))
type Route
= Home
| Users
| User Int
```
### Visibility and Encapsulation
```elm
-- Types.elm: Hide implementation, expose interface
module Email exposing (Email, fromString, toString)
-- Opaque type: Constructor is NOT exposed
type Email = Email String
-- Only way to create an Email is through validation
fromString : String -> Maybe Email
fromString str =
if String.contains "@" str then
Just (Email str)
else
Nothing
-- Only way to get the string back
toString : Email -> String
toString (Email str) =
str
-- Users CANNOT:
-- Email "invalid" -- Error: Email constructor not exposed
-- case email of Email str -> str -- Error: Cannot pattern match
-- Users CAN:
-- Email.fromString "test@example.com" |> Maybe.map Email.toString
```
### Avoiding Circular Imports
```elm
-- Problem: A imports B, B imports A → IMPORT CYCLE error
-- Solution: Extract shared types to separate module
-- Before (circular):
-- User.elm imports Main (for Msg)
-- Main.elm imports User (for User type)
-- After (fixed):
-- Types.elm (shared types)
module Types exposing (User, Msg(..))
type alias User = { name : String }
type Msg = SetUser User | LoadUsers
-- User.elm imports Types
module User exposing (view)
import Types exposing (User, Msg(..))
-- Main.elm imports Types
module Main exposing (main)
import Types exposing (User, Msg(..))
```
---
## Zero and Default Values
Elm takes a fundamentally different approach to "zero" or "default" values compared to most languages. **There are no nulls, no undefined, and no implicit defaults.**
### No Null/Undefined/Nil
```elm
-- Elm has NO:
-- - null
-- - undefined
-- - nil
-- - None (unless you define it)
-- - default constructors
-- Every value MUST be explicitly initialized
-- Every field MUST have a value
-- This is INVALID:
-- user = { name = "Alice" } -- Error: missing email, age fields
-- This is VALID:
type alias User =
{ name : String
, email : String
, age : Int
}
user : User
user =
{ name = "Alice"
, email = "alice@example.com"
, age = 30
}
```
### Maybe for Optional Values
```elm
-- Instead of null, use Maybe for values that might not exist
type Maybe a
= Just a
| Nothing
-- Optional field
type alias UserProfile =
{ name : String
, nickname : Maybe String -- Explicit: might not have one
, bio : Maybe String -- Explicit: might not have one
}
-- Creating with optional fields
profile : UserProfile
profile =
{ name = "Alice"
, nickname = Nothing -- Explicitly no nickname
, bio = Just "Developer" -- Has a bio
}
-- Must handle both cases - compiler enforces this
displayNickname : UserProfile -> String
displayNickname user =
case user.nickname of
Just nick ->
nick
Nothing ->
user.name -- Fallback to name
```
### Providing Defaults Explicitly
```elm
-- Pattern: Create "empty" or "default" values as functions
emptyUser : User
emptyUser =
{ name = ""
, email = ""
, age = 0
}
-- Pattern: Defaults with Maybe
withDefault : a -> Maybe a -> a
withDefault default maybe =
case maybe of
Just value ->
value
Nothing ->
default
-- Usage
age : Int
age =
user.age
|> String.toInt
|> Maybe.withDefault 0 -- Explicit default
-- Pattern: Defaults with Result
defaultOnError : a -> Result error a -> a
defaultOnError default result =
case result of
Ok value ->
value
Err _ ->
default
```
### Comparison to Other Languages
```elm
-- JavaScript:
-- const name = user?.name ?? "Anonymous"
-- const items = response.data || []
-- Elm equivalent:
name : String
name =
user
|> Maybe.map .name
|> Maybe.withDefault "Anonymous"
items : List Item
items =
response.data
|> Result.withDefault []
-- TypeScript:
-- interface User { name?: string } -- Optional property
-- Elm: Make it explicit
type alias User =
{ name : Maybe String } -- Explicit Maybe
-- Or use variant types for more control
type UserName
= Anonymous
| Named String
```
### Why This Matters for Conversion
```elm
-- When converting FROM languages with nulls:
-- 1. Identify optional values → use Maybe
-- 2. Identify fallback patterns → use withDefault
-- 3. Identify nullable parameters → use Maybe parameters
-- 4. Identify nullable returns → use Maybe returns
-- When converting TO languages with nulls:
-- 1. Maybe Nothing → null/None/nil
-- 2. Maybe (Just x) → x
-- 3. Maybe.withDefault default → ?? or || operators
```
---
## The Elm Architecture (TEA)
The Elm Architecture is the core pattern for all Elm applications. It consists of Model, View, and Update.
### Basic Structure
```elm
module Main exposing (main)
import Browser
import Html exposing (Html, button, div, text)
import Html.Events exposing (onClick)
-- MODEL
type alias Model =
{ count : Int
}
init : () -> ( Model, Cmd Msg )
init _ =
( { count = 0 }, Cmd.none )
-- UPDATE
type Msg
= Increment
| Decrement
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Increment ->
( { model | count = model.count + 1 }, Cmd.none )
Decrement ->
( { model | count = model.count - 1 }, Cmd.none )
-- VIEW
view : Model -> Html Msg
view model =
div []
[ button [ onClick Decrement ] [ text "-" ]
, div [] [ text (String.fromInt model.count) ]
, button [ onClick Increment ] [ text "+" ]
]
-- MAIN
main : Program () Model Msg
main =
Browser.element
{ init = init
, update = update
, view = view
, subscriptions = \_ -> Sub.none
}
```
### TEA Flow
```
┌─────────────────────────────────────────────────┐
│ The Elm Architecture │
├─────────────────────────────────────────────────┤
│ │
│ ┌───────────┐ │
│ │ init │ ──▶ ( Model, Cmd Msg ) │
│ └───────────┘ │
│ │ │
│ ▼ │
│ ┌───────────┐ User │
│ │ Model │ ◀─── Event │
│ └───────────┘ │
│ │ │
│ ▼ │
│ ┌───────────┐ │
│ │ view │ ──▶ Html Msg │
│ └───────────┘ │
│ │ │
│ ▼ │
│ ┌───────────┐ │
│ │ Msg │ (user clicks button) │
│ └───────────┘ │
│ │ │
│ ▼ │
│ ┌───────────┐ │
│ │ update │ ──▶ ( Model, Cmd Msg ) │
│ └───────────┘ │
│ │ │
│ └──────────▶ (loop back to Model) │
│ │
└─────────────────────────────────────────────────┘
```
**Key Principles:**
- Model holds all application state
- View is a pure function: Model → Html Msg
- Update is a pure function: Msg → Model → (Model, Cmd Msg)
- All side effects through Cmd (commands) and Sub (subscriptions)
---
## Core Types
### Type Aliases
```elm
-- Simple record type
type alias User =
{ name : String
, email : String
, age : Int
}
-- Creating instances
user : User
user =
{ name = "Alice"
, email = "alice@example.com"
, age = 30
}
-- Updating records (immutable)
updatedUser : User
updatedUser =
{ user | age = 31 }
-- Nested records
type alias Address =
{ street : String
, city : String
}
type alias Person =
{ name : String
, address : Address
}
```
### Union Types (Custom Types)
```elm
-- Simple union type
type Direction
= North
| South
| East
| West
-- Union type with data
type Msg
= NoOp
| SetName String
| SetAge Int
| Login { username : String, password : String }
-- Using union types
handleMsg : Msg -> Model -> Model
handleMsg msg model =
case msg of
NoOp ->
model
SetName name ->
{ model | name = name }
SetAge age ->
{ model | age = age }
Login { username, password } ->
-- Handle login
model
```
### Maybe and Result
```elm
-- Maybe: value that might not exist
type Maybe a
= Just a
| Nothing
-- Using Maybe
findUser : Int -> Maybe User
findUser id =
if id == 1 then
Just { name = "Alice", email = "alice@example.com", age = 30 }
else
Nothing
-- Pattern matching Maybe
displayName : Maybe User -> String
displayName maybeUser =
case maybeUser of
Just user ->
user.name
Nothing ->
"Anonymous"
-- Maybe helpers
name : String
name =
findUser 1
|> Maybe.map .name
|> Maybe.withDefault "Anonymous"
-- Result: operation that might fail
type Result error value
= Ok value
| Err error
-- Using Result
parseAge : String -> Result String Int
parseAge str =
case String.toInt str of
Just age ->
if age >= 0 then
Ok age
else
Err "Age must be non-negative"
Nothing ->
Err "Not a valid number"
-- Chaining Results
validateAge : String -> Result String Int
validateAge str =
parseAge str
|> Result.andThen (\age ->
if age < 120 then
Ok age
else
Err "Age must be less than 120"
)
```
---
## Pattern Matching
### Case Expressions
```elm
-- Basic case
describeNumber : Int -> String
describeNumber n =
case n of
0 ->
"zero"
1 ->
"one"
_ ->
"other"
-- Multiple patterns
describeList : List a -> String
describeList list =
case list of
[] ->
"empty"
[ x ] ->
"singleton"
[ x, y ] ->
"pair"
x :: xs ->
"list with multiple elements"
-- Destructuring records
greet : User -> String
greet user =
case user of
{ name, age } ->
"Hello " ++ name ++ ", age " ++ String.fromInt age
```
### If-Let Pattern
```elm
-- Guards in case expressions
classify : Int -> String
classify n =
case n of
x ->
if x < 0 then
"negative"
else if x == 0 then
"zero"
else
"positive"
-- Let-in for local bindings
calculate : Int -> Int
calculate x =
let
doubled =
x * 2
squared =
x * x
in
doubled + squared
```
---
## Functions
### Function Syntax
```elm
-- Simple function
add : Int -> Int -> Int
add x y =
x + y
-- Anonymous function (lambda)
increment : Int -> Int
increment =
\x -> x + 1
-- Partial application
add5 : Int -> Int
add5 =
add 5
-- Pipeline operator
result : Int
result =
10
|> add 5
|> multiply 2
|> subtract 3
-- Composition operator
addThenDouble : Int -> Int -> Int
addThenDouble =
add >> multiply 2
```
### Higher-Order Functions
```elm
-- Map
doubled : List Int
doubled =
List.map (\x -> x * 2) [ 1, 2, 3, 4, 5 ]
-- Filter
evens : List Int
evens =
List.filter (\x -> modBy 2 x == 0) [ 1, 2, 3, 4, 5 ]
-- Fold (reduce)
sum : Int
sum =
List.foldl (+) 0 [ 1, 2, 3, 4, 5 ]
-- Chain operations
processNumbers : List Int -> Int
processNumbers numbers =
numbers
|> List.filter (\x -> x > 0)
|> List.map (\x -> x * 2)
|> List.foldl (+) 0
```
---
## Working with JSON
### JSON Decoders
```elm
import Json.Decode as Decode exposing (Decoder)
-- Simple decoder
userDecoder : Decoder User
userDecoder =
Decode.map3 User
(Decode.field "name" Decode.string)
(Decode.field "email" Decode.string)
(Decode.field "age" Decode.int)
-- Alternative syntax with succeed and pipeline
import Json.Decode.Pipeline exposing (required, optional)
userDecoder : Decoder User
userDecoder =
Decode.succeed User
|> required "name" Decode.string
|> required "email" Decode.string
|> required "age" Decode.int
-- Decoding lists
usersDecoder : Decoder (List User)
usersDecoder =
Decode.list userDecoder
-- Decoding nested objects
type alias Response =
{ data : List User
, total : Int
}
responseDecoder : Decoder Response
responseDecoder =
Decode.map2 Response
(Decode.field "data" (Decode.list userDecoder))
(Decode.field "total" Decode.int)
-- Optional fields
type alias Config =
{ apiUrl : String
, timeout : Maybe Int
}
configDecoder : Decoder Config
configDecoder =
Decode.map2 Config
(Decode.field "apiUrl" Decode.string)
(Decode.maybe (Decode.field "timeout" Decode.int))
```
### JSON Encoders
```elm
import Json.Encode as Encode
-- Encode user to JSON
encodeUser : User -> Encode.Value
encodeUser user =
Encode.object
[ ( "name", Encode.string user.name )
, ( "email", Encode.string user.email )
, ( "age", Encode.int user.age )
]
-- Encode list
encodeUsers : List User -> Encode.Value
encodeUsers users =
Encode.list encodeUser users
-- Optional fields
encodeConfig : Config -> Encode.Value
encodeConfig config =
case config.timeout of
Just timeout ->
Encode.object
[ ( "apiUrl", Encode.string config.apiUrl )
, ( "timeout", Encode.int timeout )
]
Nothing ->
Encode.object
[ ( "apiUrl", Encode.string config.apiUrl )
]
```
---
## HTTP Requests
### Making HTTP Requests
```elm
import Http
import Json.Decode as Decode
-- GET request
type Msg
= GotUsers (Result Http.Error (List User))
getUsers : Cmd Msg
getUsers =
Http.get
{ url = "https://api.example.com/users"
, expect = Http.expectJson GotUsers (Decode.list userDecoder)
}
-- POST request
createUser : User -> Cmd Msg
createUser user =
Http.post
{ url = "https://api.example.com/users"
, body = Http.jsonBody (encodeUser user)
, expect = Http.expectJson GotCreateUserResponse 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 GotUpdateUserResponse userDecoder
, timeout = Nothing
, tracker = Nothing
}
-- Handling HTTP results in update
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GotUsers result ->
case result of
Ok users ->
( { model | users = users, error = Nothing }, Cmd.none )
Err error ->
( { model | error = Just (httpErrorToString error) }, Cmd.none )
-- HTTP error handling
httpErrorToString : Http.Error -> String
httpErrorToString error =
case error of
Http.BadUrl url ->
"Bad URL: " ++ url
Http.Timeout ->
"Request timed out"
Http.NetworkError ->
"Network error"
Http.BadStatus status ->
"Bad status: " ++ String.fromInt status
Http.BadBody body ->
"Bad body: " ++ body
```
---
## Common Patterns
### Loading States
```elm
type RemoteData error value
= NotAsked
| Loading
| Success value
| Failure error
type alias Model =
{ users : RemoteData Http.Error (List User)
}
-- In update
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
FetchUsers ->
( { model | users = Loading }, getUsers )
GotUsers result ->
case result of
Ok users ->
( { model | users = Success users }, Cmd.none )
Err error ->
( { model | users = Failure error }, Cmd.none )
-- In view
view : Model -> Html Msg
view model =
case model.users of
NotAsked ->
button [ onClick FetchUsers ] [ text "Load Users" ]
Loading ->
div [] [ text "Loading..." ]
Success users ->
div [] (List.map viewUser users)
Failure error ->
div [] [ text ("Error: " ++ httpErrorToString error) ]
```
### Form Handling
```elm
type alias Form =
{ name : String
, email : String
, errors : List String
}
type Msg
= SetName String
| SetEmail String
| Submit
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
SetName name ->
( { model | form = updateFormName name model.form }, Cmd.none )
SetEmail email ->
( { model | form = updateFormEmail email model.form }, Cmd.none )
Submit ->
case validateForm model.form of
Ok validForm ->
( model, submitForm validForm )
Err errors ->
( { model | form = setFormErrors errors model.form }, Cmd.none )
-- View with form
viewForm : Form -> Html Msg
viewForm form =
div []
[ input
[ type_ "text"
, placeholder "Name"
, value form.name
, onInput SetName
]
[]
, input
[ type_ "email"
, placeholder "Email"
, value form.email
, onInput SetEmail
]
[]
, button [ onClick Submit ] [ text "Submit" ]
, viewErrors form.errors
]
viewErrors : List String -> Html msg
viewErrors errors =
div []
(List.map (\error -> div [] [ text error ]) errors)
```
### Routing
```elm
import Browser.Navigation as Nav
import Url
import Url.Parser as Parser exposing (Parser, (</>))
type Route
= Home
| Users
| User Int
| NotFound
routeParser : Parser (Route -> a) a
routeParser =
Parser.oneOf
[ Parser.map Home Parser.top
, Parser.map Users (Parser.s "users")
, Parser.map User (Parser.s "users" </> Parser.int)
]
fromUrl : Url.Url -> Route
fromUrl url =
Parser.parse routeParser url
|> Maybe.withDefault NotFound
-- In application
type alias Model =
{ key : Nav.Key
, route : Route
}
type Msg
= UrlChanged Url.Url
| LinkClicked Browser.UrlRequest
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
LinkClicked urlRequest ->
case urlRequest of
Browser.Internal url ->
( model, Nav.pushUrl model.key (Url.toString url) )
Browser.External href ->
( model, Nav.load href )
UrlChanged url ->
( { model | route = fromUrl url }, Cmd.none )
```
---
## Elm Tooling
### elm.json
```json
{
"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": {}
},
"test-dependencies": {
"direct": {},
"indirect": {}
}
}
```
### Commands
```bash
# Initialize new project
elm init
# Install package
elm install elm/http
# Build
elm make src/Main.elm
# Build optimized
elm make src/Main.elm --optimize --output=main.js
# Start development server
elm reactor
# Run tests
elm-test
# Format code
elm-format src/ --yes
# Review code
elm-review
```
---
## Testing
Elm has a mature testing ecosystem with elm-test that emphasizes property-based testing (fuzz testing) alongside traditional unit tests. The strong type system eliminates many bugs, but tests remain valuable for logic and behavior verification.
### Basic Unit Tests
```elm
module Tests exposing (..)
import Expect exposing (Expectation)
import Test exposing (Test, describe, test)
import MyModule exposing (add, parseAge)
-- Simple expectation tests
suite : Test
suite =
describe "MyModule"
[ describe "add"
[ test "adds two positive numbers" <|
\_ ->
add 2 3
|> Expect.equal 5
, test "adds negative numbers" <|
\_ ->
add (-2) 3
|> Expect.equal 1
, test "identity with zero" <|
\_ ->
add 0 5
|> Expect.equal 5
]
, describe "parseAge"
[ test "parses valid age" <|
\_ ->
parseAge "25"
|> Expect.equal (Ok 25)
, test "rejects negative age" <|
\_ ->
parseAge "-5"
|> Expect.err
, test "rejects non-numeric input" <|
\_ ->
parseAge "abc"
|> Expect.err
]
]
```
### Fuzz Testing (Property-Based Testing)
```elm
import Fuzz exposing (Fuzzer, int, intRange, list, string)
import Test exposing (Test, describe, fuzz, fuzz2)
fuzzSuite : Test
fuzzSuite =
describe "Property-based tests"
[ fuzz int "add is commutative" <|
\x ->
add x 5
|> Expect.equal (add 5 x)
, fuzz2 int int "add is associative" <|
\x y ->
add x y
|> Expect.equal (add y x)
, fuzz (intRange 0 150) "valid ages are accepted" <|
\age ->
parseAge (String.fromInt age)
|> Expect.ok
, fuzz string "parseAge handles any string" <|
\str ->
-- Should never crash, always returns Result
case parseAge str of
Ok age ->
age |> Expect.atLeast 0
Err _ ->
Expect.pass
]
-- Custom Fuzzers
userFuzzer : Fuzzer User
userFuzzer =
Fuzz.map3 User
Fuzz.string -- name
(Fuzz.map (\s -> s ++ "@example.com") Fuzz.string) -- email
(Fuzz.intRange 0 120) -- age
userListFuzzer : Fuzzer (List User)
userListFuzzer =
Fuzz.list userFuzzer
```
### Testing The Elm Architecture
```elm
import Test exposing (Test, describe, test)
import Main exposing (Model, Msg(..), update, init)
-- Test update function
updateTests : Test
updateTests =
describe "update"
[ test "Increment increases count" <|
\_ ->
let
( model, _ ) =
init ()
( newModel, _ ) =
update Increment model
in
newModel.count
|> Expect.equal 1
, test "Decrement decreases count" <|
\_ ->
let
model =
{ count = 5 }
( newModel, _ ) =
update Decrement model
in
newModel.count
|> Expect.equal 4
, test "Reset sets count to zero" <|
\_ ->
let
model =
{ count = 100 }
( newModel, _ ) =
update Reset model
in
newModel.count
|> Expect.equal 0
]
-- Test that update returns expected commands
commandTests : Test
commandTests =
describe "commands"
[ test "FetchUsers returns Http command" <|
\_ ->
let
( _, cmd ) =
update FetchUsers initialModel
in
cmd
|> Expect.notEqual Cmd.none
, test "Increment returns no command" <|
\_ ->
let
( _, cmd ) =
update Increment initialModel
in
cmd
|> Expect.equal Cmd.none
]
```
### Testing JSON Decoders
```elm
import Json.Decode as Decode
import Test exposing (Test, describe, test)
import Expect
decoderTests : Test
decoderTests =
describe "JSON Decoders"
[ test "decodes valid user JSON" <|
\_ ->
let
json =
"""{"name": "Alice", "email": "alice@example.com", "age": 30}"""
in
Decode.decodeString userDecoder json
|> Expect.equal
(Ok { name = "Alice", email = "alice@example.com", age = 30 })
, test "fails on missing field" <|
\_ ->
let
json =
"""{"name": "Alice", "age": 30}"""
in
Decode.decodeString userDecoder json
|> Expect.err
, test "fails on wrong type" <|
\_ ->
let
json =
"""{"name": "Alice", "email": "alice@example.com", "age": "thirty"}"""
in
Decode.decodeString userDecoder json
|> Expect.err
, test "decodes list of users" <|
\_ ->
let
json =
"""[{"name": "Alice", "email": "a@b.com", "age": 30}]"""
in
Decode.decodeString (Decode.list userDecoder) json
|> Result.map List.length
|> Expect.equal (Ok 1)
]
```
### Test Organization
```bash
# Recommended test structure
tests/
├── Tests.elm # Main test module, imports all
├── UnitTests/
│ ├── UserTests.elm # Tests for User module
│ ├── ApiTests.elm # Tests for API module
│ └── UtilTests.elm # Tests for utilities
└── FuzzTests/
└── PropertyTests.elm # Fuzz/property tests
```
```elm
-- tests/Tests.elm
module Tests exposing (suite)
import Test exposing (Test, describe)
import UnitTests.UserTests
import UnitTests.ApiTests
import FuzzTests.PropertyTests
suite : Test
suite =
describe "All Tests"
[ UnitTests.UserTests.suite
, UnitTests.ApiTests.suite
, FuzzTests.PropertyTests.suite
]
```
### Running Tests
```bash
# Install elm-test
npm install -g elm-test
# Initialize tests in project
elm-test init
# Run all tests
elm-test
# Run tests in watch mode
elm-test --watch
# Run specific test file
elm-test tests/UserTests.elm
# Run with different seed (for fuzz tests)
elm-test --seed 12345
# Run with more fuzz iterations
elm-test --fuzz 1000
```
### Expectation Helpers
```elm
import Expect exposing (Expectation)
-- Equality
Expect.equal expected actual
Expect.notEqual unexpected actual
-- Comparisons
Expect.lessThan upper actual
Expect.atMost upper actual
Expect.greaterThan lower actual
Expect.atLeast lower actual
-- Floating point (with tolerance)
Expect.within (Expect.Absolute 0.001) 3.14159 pi
-- Boolean
Expect.true "should be true" condition
Expect.false "should be false" condition
-- Result/Maybe
Expect.ok result -- Result is Ok
Expect.err result -- Result is Err
Expect.just maybe -- Maybe is Just (Elm 0.19+)
Expect.nothing maybe -- Maybe is Nothing
-- Lists
Expect.equalLists expected actual
-- Custom failure
Expect.fail "Custom failure message"
Expect.pass
```
### What Tests Actually Catch in Elm
```elm
-- Types catch these (NO tests needed):
-- - Null pointer exceptions (no null in Elm)
-- - Type mismatches (compiler catches)
-- - Missing pattern matches (compiler catches)
-- - Undefined functions (compiler catches)
-- Tests catch these (tests valuable):
-- - Business logic errors
-- - Off-by-one errors
-- - Edge cases (empty lists, zero, negative numbers)
-- - JSON decode/encode round-trips
-- - Algorithm correctness
-- - State machine transitions (TEA update logic)
-- Example: Type system prevents this, no test needed
getName : User -> String -- Can NEVER be called with null
-- Example: Test needed for business logic
isEligible : User -> Bool
isEligible user =
user.age >= 18 && user.verified
-- Test: age=17, verified=true → false
-- Test: age=18, verified=false → false
-- Test: age=18, verified=true → true
```
---
## Troubleshooting
### Type Mismatch Errors
**Problem:** `The 1st argument to map is not what I expect`
```elm
-- Error: Wrong type
List.map String.toInt [ "1", "2", "3" ] -- Returns List (Maybe Int)
-- Fix: Handle Maybe
List.filterMap String.toInt [ "1", "2", "3" ] -- Returns List Int
```
### Missing Pattern Match
**Problem:** `This case expression does not have branches for all possibilities`
```elm
-- Error: Missing Nothing case
getName : Maybe User -> String
getName maybeUser =
case maybeUser of
Just user ->
user.name
-- Fix: Add all cases
getName : Maybe User -> String
getName maybeUser =
case maybeUser of
Just user ->
user.name
Nothing ->
"Anonymous"
```
### Circular Dependencies
**Problem:** `IMPORT CYCLE`
**Fix:** Extract shared types to separate module:
```elm
-- Before: Main.elm imports User.elm, User.elm imports Main.elm
-- After: Create Types.elm
module Types exposing (User, Msg)
-- Main.elm imports Types
-- User.elm imports Types
```
---
## Concurrency
Elm takes a fundamentally different approach to concurrency compared to traditional imperative languages. Rather than managing threads and locks, Elm's architecture handles concurrency through managed effects. For cross-language comparison, see `patterns-concurrency-dev`.
### Why Traditional Concurrency Doesn't Apply
```elm
-- Elm has NO:
-- - Threads or green threads
-- - Locks or mutexes
-- - Race conditions
-- - Shared mutable state
-- - Async/await keywords
-- Instead: The Elm Runtime manages ALL concurrency
-- Your code is ALWAYS single-threaded and synchronous
```
### The Elm Architecture Handles Concurrency
```elm
-- Multiple HTTP requests "in flight" - runtime manages them
type Msg
= GotUser1 (Result Http.Error User)
| GotUser2 (Result Http.Error User)
| GotUser3 (Result Http.Error User)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
StartFetching ->
-- Runtime executes these concurrently
( { model | loading = True }
, Cmd.batch
[ Http.get { url = "/api/user/1", expect = Http.expectJson GotUser1 userDecoder }
, Http.get { url = "/api/user/2", expect = Http.expectJson GotUser2 userDecoder }
, Http.get { url = "/api/user/3", expect = Http.expectJson GotUser3 userDecoder }
]
)
GotUser1 result ->
-- Each response handled independently as it arrives
-- Runtime ensures update is never called concurrently
handleUserResult 1 result model
GotUser2 result ->
handleUserResult 2 result model
GotUser3 result ->
handleUserResult 3 result model
```
### Cmd and Sub: Managed Effects
```elm
-- Cmd: Commands that produce effects
-- The runtime executes these, guarantees serialized updates
type alias Model =
{ time : Time.Posix
, windowSize : ( Int, Int )
}
type Msg
= Tick Time.Posix
| WindowResized Int Int
-- Subscriptions: Stream of events from outside world
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.batch
[ Time.every 1000 Tick -- Every second
, Browser.Events.onResize WindowResized -- On window resize
]
-- Runtime manages these subscriptions
-- Messages arrive one at a time in update function
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Tick newTime ->
( { model | time = newTime }, Cmd.none )
WindowResized width height ->
( { model | windowSize = ( width, height ) }, Cmd.none )
```
### Task: Sequential Async Operations
```elm
import Task exposing (Task)
import Time
import Http
-- Task: Describes async operation, doesn't execute until performed
-- Think of it as a "recipe" for an async operation
getCurrentTime : Task Never Time.Posix
getCurrentTime =
Time.now
-- Chain Tasks sequentially
fetchAndLog : Task Http.Error String
fetchAndLog =
Http.task
{ method = "GET"
, headers = []
, url = "/api/data"
, body = Http.emptyBody
, resolver = Http.stringResolver handleResponse
, timeout = Nothing
}
|> Task.andThen (\data ->
-- Log happens AFTER fetch completes
Task.succeed ("Fetched: " ++ data)
)
-- Perform task to create Cmd
type Msg
= GotData (Result Http.Error String)
fetchData : Cmd Msg
fetchData =
Task.attempt GotData fetchAndLog
-- Task combinators for "concurrent" execution
-- (Runtime manages, you describe relationships)
fetchMultiple : Cmd Msg
fetchMultiple =
Task.map2 Tuple.pair
(Http.task { ... }) -- Fetch 1
(Http.task { ... }) -- Fetch 2
|> Task.attempt GotBothResults
-- Runtime may execute concurrently, delivers result when BOTH complete
```
### Process: Background Tasks
```elm
import Process
import Task
-- Delay execution
delayed : Msg -> Cmd Msg
delayed msg =
Process.sleep 1000 -- 1 second in milliseconds
|> Task.perform (\_ -> msg)
-- Debouncing user input
type Msg
= UserTyped String
| DebouncedInput String
| CancelDebounce
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
UserTyped input ->
( { model | input = input }
, Cmd.batch
[ Process.sleep 500
|> Task.perform (\_ -> DebouncedInput input)
]
)
DebouncedInput input ->
-- Only fires if user stops typing for 500ms
( model, performSearch input )
```
### Ports: Concurrent JavaScript Interop
```elm
-- port: Escape hatch for JS concurrency primitives
port module Main exposing (..)
-- Outgoing: Send to JavaScript
port sendToWorker : String -> Cmd msg
-- Incoming: Receive from JavaScript (subscription)
port workerResponse : (String -> msg) -> Sub msg
type Msg
= StartWork String
| WorkComplete String
subscriptions : Model -> Sub Msg
subscriptions model =
workerResponse WorkComplete
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
StartWork data ->
-- JavaScript can run Web Workers, handle concurrency
( { model | working = True }
, sendToWorker data
)
WorkComplete result ->
( { model | working = False, result = result }
, Cmd.none
)
-- In JavaScript:
-- app.ports.sendToWorker.subscribe(function(data) {
-- const worker = new Worker('worker.js');
-- worker.postMessage(data);
-- worker.onmessage = function(e) {
-- app.ports.workerResponse.send(e.data);
-- };
-- });
```
### Key Principles
```elm
-- 1. Update is NEVER concurrent - always single-threaded
-- 2. Runtime manages ALL asynchronous operations
-- 3. No race conditions possible in Elm code
-- 4. Cmd/Sub provide declarative concurrency
-- 5. For CPU-intensive work: Use ports + Web Workers
-- Compare to other languages:
-- - Go/Erlang: Explicit goroutines/processes → Elm: Cmd/Sub
-- - JavaScript: async/await → Elm: Task
-- - Rust: threads + channels → Elm: Runtime + messages
```
---
## Metaprogramming
Elm intentionally does not support traditional metaprogramming like macros or reflection. This is a deliberate design choice to ensure code is explicit, maintainable, and debuggable. For cross-language comparison, see `patterns-metaprogramming-dev`.
### Why Elm Has No Metaprogramming
```elm
-- Elm has NO:
-- - Macros (no code that writes code)
-- - Reflection (can't inspect types at runtime)
-- - eval() (no runtime code execution)
-- - Dynamic code generation
-- - Preprocessor directives
-- Philosophy:
-- - Explicit is better than implicit
-- - Code should be readable without magic
-- - Compiler guarantees should be reliable
-- - Refactoring should be safe and predictable
```
### Alternative: Code Generation (elm-codegen)
```bash
# External tool generates Elm code at build time
# NOT metaprogramming - generates source files you commit
# Example: Generate API client from OpenAPI spec
elm-codegen openapi.yaml --output src/Generated/Api.elm
# Result: Normal Elm code you can read and version control
```
```elm
-- Generated/Api.elm (example output)
module Generated.Api exposing (getUser, createUser)
import Http
import Json.Decode as Decode
getUser : Int -> (Result Http.Error User -> msg) -> Cmd msg
getUser userId toMsg =
Http.get
{ url = "/api/users/" ++ String.fromInt userId
, expect = Http.expectJson toMsg userDecoder
}
userDecoder : Decode.Decoder User
userDecoder =
Decode.map3 User
(Decode.field "name" Decode.string)
(Decode.field "email" Decode.string)
(Decode.field "age" Decode.int)
```
### Alternative: Type-Driven Design
```elm
-- Instead of metaprogramming, use types to enforce invariants
-- Bad: Use strings, need validation everywhere
type alias UserId = String
validateUserId : String -> Maybe UserId
validateUserId str =
if String.startsWith "user-" str then
Just str
else
Nothing
-- Good: Make invalid states unrepresentable
type UserId = UserId String
createUserId : String -> Maybe UserId
createUserId str =
if String.startsWith "user-" str then
Just (UserId str)
else
Nothing
getUserById : UserId -> Cmd Msg
getUserById (UserId id) =
-- Guaranteed to be valid, no runtime checks needed
Http.get { url = "/api/users/" ++ id, ... }
-- Type system does the "metaprogramming" work at compile time
```
### Alternative: Phantom Types
```elm
-- Encode state in types without runtime cost
type Validated
type Unvalidated
type Form a
= Form
{ email : String
, age : String
}
-- Can't use unvalidated form
submitForm : Form Validated -> Cmd Msg
submitForm (Form data) =
Http.post { ... }
-- Must validate first
validateForm : Form Unvalidated -> Result (List String) (Form Validated)
validateForm (Form data) =
if String.contains "@" data.email then
Ok (Form data)
else
Err [ "Invalid email" ]
-- Type system prevents: submitForm unvalidatedForm
-- Type system enforces: validateForm form |> Result.map submitForm
```
### Alternative: Custom Types for DSLs
```elm
-- Instead of macros, define DSL as data
type Query
= Select (List String) Table (Maybe Condition)
| Insert Table (List ( String, Value ))
type Table = Table String
type Condition
= Equals String Value
| And Condition Condition
| Or Condition Condition
type Value
= StringVal String
| IntVal Int
-- Build queries as data
query : Query
query =
Select
[ "name", "email" ]
(Table "users")
(Just (Equals "active" (StringVal "true")))
-- Interpret DSL
toSql : Query -> String
toSql queryData =
case queryData of
Select fields (Table tableName) maybeWhere ->
"SELECT "
++ String.join ", " fields
++ " FROM "
++ tableName
++ (case maybeWhere of
Just condition ->
" WHERE " ++ conditionToSql condition
Nothing ->
""
)
_ ->
"..."
-- Result: Type-safe SQL without macros
-- Compiler checks all queries at compile time
```
### Ports as FFI (Foreign Function Interface)
```elm
-- Port: Call JavaScript for things Elm can't do
port module Analytics exposing (trackEvent)
-- Send data to JavaScript
port trackEvent : { name : String, properties : Json.Encode.Value } -> Cmd msg
-- Use like any other Cmd
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
ButtonClicked ->
( model
, trackEvent
{ name = "button_clicked"
, properties =
Json.Encode.object
[ ( "button_id", Json.Encode.string "submit" )
]
}
)
```
```javascript
// JavaScript side (ports.js)
app.ports.trackEvent.subscribe(function(event) {
// Use any JS metaprogramming/reflection here
analytics.track(event.name, event.properties);
// Can use JS features Elm doesn't have:
// - eval()
// - Proxy objects
// - Reflect API
// - Dynamic code loading
});
```
### elm-review for Custom Linting
```elm
-- Instead of macros, write custom compile-time checks
-- elm-review: Analyze and transform code at build time
module ReviewConfig exposing (config)
import Review.Rule as Rule
import Elm.Syntax.Expression exposing (Expression)
-- Custom rule: Prevent Debug.log in production
noDebugLog : Rule
noDebugLog =
Rule.newModuleRuleSchema "NoDebugLog" ()
|> Rule.withSimpleExpressionVisitor expressionVisitor
|> Rule.fromModuleRuleSchema
expressionVisitor : Expression -> List Rule.Error
expressionVisitor expression =
case expression of
FunctionOrValue [ "Debug" ] "log" ->
[ Rule.error
{ message = "Debug.log is not allowed"
, details = [ "Remove Debug.log before committing" ]
}
]
_ ->
[]
```
### Key Principles
```elm
-- Elm philosophy on metaprogramming:
-- 1. No magic - code should be obvious
-- 2. Use types instead of runtime checks
-- 3. Generate code externally, commit it
-- 4. Ports for JS interop when needed
-- 5. elm-review for custom compile-time checks
-- Compare to other languages:
-- - Ruby/Lisp macros → Elm: Code generation + types
-- - Python reflection → Elm: Phantom types
-- - Template Haskell → Elm: External codegen
-- - C preprocessor → Elm: Type system + elm-review
```
---
## Cross-Cutting Patterns
For cross-language comparison and translation patterns, see:
- `patterns-concurrency-dev` - Compare Elm's Cmd/Sub/Task to threads, async/await, actors
- `patterns-metaprogramming-dev` - Compare Elm's type-driven approach to macros, reflection, codegen
- `patterns-serialization-dev` - JSON decoders/encoders patterns across languages
---
## References
- [Elm Guide](https://guide.elm-lang.org/) - Official tutorial
- [Elm Packages](https://package.elm-lang.org/) - Package repository
- [Elm Discourse](https://discourse.elm-lang.org/) - Community forum
- [Elm Radio Podcast](https://elm-radio.com/) - Elm news and discussions
- [Elm in Action](https://www.manning.com/books/elm-in-action) - Comprehensive book