| name | elm-development |
| description | Comprehensive Elm functional programming for web development including The Elm Architecture, type system, JavaScript interop, and production-ready patterns |
Elm Development
A comprehensive skill for building reliable web applications using Elm, a functional programming language that compiles to JavaScript. Elm offers no runtime errors in practice, friendly compiler messages, and a delightful development experience through strong typing and pure functional programming.
When to Use This Skill
Use this skill when:
- Building web applications that require high reliability and zero runtime errors
- Creating SPAs (Single Page Applications) with predictable state management
- Developing user interfaces with strong type safety guarantees
- Working on projects where refactoring safety is critical
- Building applications with complex state that needs to be managed cleanly
- Creating web apps that benefit from functional programming patterns
- Developing projects where friendly error messages aid rapid development
- Integrating Elm components into existing JavaScript applications
- Building interactive web applications with real-time features
- Creating maintainable, long-lived codebases with clear contracts
Core Concepts
The Elm Architecture (TEA)
The Elm Architecture is a pattern for building interactive applications that emerged naturally from Elm development. It inspired Redux and similar state management patterns.
Core Components:
- Model: The complete state of your application
- View: A pure function that renders the model as HTML
- Update: A pure function that updates the model based on messages
Data Flow:
Model → View → HTML
↑ ↓
└── Update ←──┘
Messages
Basic Pattern:
type alias Model =
{ count : Int }
type Msg
= Increment
| Decrement
init : Model
init =
{ count = 0 }
update : Msg -> Model -> Model
update msg model =
case msg of
Increment ->
{ model | count = model.count + 1 }
Decrement ->
{ model | count = model.count - 1 }
view : Model -> Html Msg
view model =
div []
[ button [ onClick Decrement ] [ text "-" ]
, div [] [ text (String.fromInt model.count) ]
, button [ onClick Increment ] [ text "+" ]
]
Extended Architecture with Effects
For real applications, you need side effects (HTTP, random, etc.):
Enhanced Components:
- init: Returns
(Model, Cmd Msg)- initial state plus commands to run - update: Returns
(Model, Cmd Msg)- new state plus commands to run - subscriptions: Listens to external events (time, websockets, etc.)
- view: Same pure function rendering HTML
Program Types:
Browser.sandbox: Simple apps without side effectsBrowser.element: Apps with side effects (HTTP, etc.)Browser.document: Control over<title>and<body>Browser.application: Full SPA with URL routing
Type System Fundamentals
Elm's type system prevents runtime errors through static analysis and type inference.
Type Annotations:
-- Function type annotations
add : Int -> Int -> Int
add x y = x + y
-- Record type alias
type alias User =
{ id : Int
, name : String
, email : String
}
-- Custom types (sum types / tagged unions)
type UserStatus
= Active
| Suspended
| Deleted
-- Parametric types
type Result error value
= Ok value
| Err error
Type Inference: Elm infers types automatically, but annotations are recommended for top-level functions:
-- Compiler infers: String -> String
greet name =
"Hello, " ++ name
-- Better: Add explicit annotation
greet : String -> String
greet name =
"Hello, " ++ name
Benefits:
- Catches type errors at compile time
- Provides excellent error messages
- Enables fearless refactoring
- Documents function signatures
- No null/undefined errors
Maybe and Result Types
Elm eliminates null/undefined errors using Maybe and Result.
Maybe Type:
type Maybe a
= Just a
| Nothing
-- Example: Finding a user
findUser : Int -> Maybe User
findUser id =
-- Returns Just user if found, Nothing otherwise
...
-- Pattern matching on Maybe
case findUser 123 of
Just user ->
"Found: " ++ user.name
Nothing ->
"User not found"
-- Helper functions
Maybe.withDefault : a -> Maybe a -> a
Maybe.map : (a -> b) -> Maybe a -> Maybe b
Maybe.andThen : (a -> Maybe b) -> Maybe a -> Maybe b
Result Type:
type Result error value
= Ok value
| Err error
-- Example: Validating user input
validateAge : String -> Result String Int
validateAge input =
case String.toInt input of
Nothing ->
Err "Not a valid number"
Just age ->
if age < 0 then
Err "Age cannot be negative"
else if age > 150 then
Err "Age seems unrealistic"
else
Ok age
-- Using Result
case validateAge "25" of
Ok age ->
"Valid age: " ++ String.fromInt age
Err error ->
"Error: " ++ error
Pattern Matching
Pattern matching is fundamental to Elm programming:
-- Matching on custom types
type Traffic
= Red
| Yellow
| Green
describe : Traffic -> String
describe light =
case light of
Red ->
"Stop"
Yellow ->
"Prepare to stop"
Green ->
"Go"
-- Matching on lists
describeList : List a -> String
describeList list =
case list of
[] ->
"Empty list"
[single] ->
"One item"
[first, second] ->
"Two items"
_ ->
"Many items"
-- Matching with extraction
type User
= Anonymous
| LoggedIn String Int
getUserName : User -> String
getUserName user =
case user of
Anonymous ->
"Guest"
LoggedIn name _ ->
name
Language Features
Functions
Function Definition:
-- Simple function
double : Int -> Int
double x =
x * 2
-- Multiple parameters
add : Int -> Int -> Int
add x y =
x + y
-- Partial application
add5 : Int -> Int
add5 =
add 5
-- Anonymous functions (lambdas)
doubleList : List Int -> List Int
doubleList numbers =
List.map (\n -> n * 2) numbers
-- Pipe operator
result : Int
result =
[1, 2, 3, 4, 5]
|> List.map double
|> List.filter (\n -> n > 5)
|> List.sum
-- Function composition
addThenDouble : Int -> Int
addThenDouble =
add 3 >> double
Let Expressions:
calculateArea : Float -> Float -> Float
calculateArea width height =
let
perimeter =
2 * (width + height)
area =
width * height
in
area
Records
Record Syntax:
-- Record type alias
type alias Point =
{ x : Float
, y : Float
}
-- Creating records
origin : Point
origin =
{ x = 0, y = 0 }
-- Accessing fields
getX : Point -> Float
getX point =
point.x
-- Field access function
.x origin -- Returns 0
-- Updating records (immutable)
moveRight : Point -> Point
moveRight point =
{ point | x = point.x + 1 }
-- Updating multiple fields
moveDiagonal : Point -> Point
moveDiagonal point =
{ point
| x = point.x + 1
, y = point.y + 1
}
-- Pattern matching on records
distance : Point -> Float
distance { x, y } =
sqrt (x * x + y * y)
Lists and Arrays
Lists (Linked Lists):
-- List literals
numbers : List Int
numbers =
[1, 2, 3, 4, 5]
-- Cons operator
moreNumbers : List Int
moreNumbers =
0 :: numbers -- [0, 1, 2, 3, 4, 5]
-- List functions
List.map : (a -> b) -> List a -> List b
List.filter : (a -> Bool) -> List a -> List a
List.foldl : (a -> b -> b) -> b -> List a -> b
List.length : List a -> Int
List.reverse : List a -> List a
List.sort : List comparable -> List comparable
List.concat : List (List a) -> List a
-- Example usage
processNumbers : List Int -> Int
processNumbers nums =
nums
|> List.filter (\n -> n > 0)
|> List.map (\n -> n * 2)
|> List.sum
Arrays (Random Access):
import Array exposing (Array)
-- Arrays for fast random access
numbers : Array Int
numbers =
Array.fromList [1, 2, 3, 4, 5]
-- Array operations
Array.get : Int -> Array a -> Maybe a
Array.set : Int -> a -> Array a -> Array a
Array.push : a -> Array a -> Array a
Array.length : Array a -> Int
Modules and Imports
Module Definition:
module Utils exposing (capitalize, truncate)
-- Private function (not exposed)
isBlank : String -> Bool
isBlank str =
String.trim str == ""
-- Public function
capitalize : String -> String
capitalize str =
if isBlank str then
str
else
String.toUpper (String.left 1 str) ++ String.dropLeft 1 str
-- Public function
truncate : Int -> String -> String
truncate maxLength str =
if String.length str > maxLength then
String.left (maxLength - 3) str ++ "..."
else
str
Import Syntax:
-- Import entire module
import List
import Dict
-- Import with alias
import Json.Decode as Decode
import Json.Encode as Encode
-- Import specific functions
import List exposing (map, filter, foldl)
-- Import all exposed functions (use sparingly)
import Html exposing (..)
The Elm Architecture in Depth
Commands (Cmd)
Commands represent side effects to perform:
type Msg
= FetchUser
| GotUser (Result Http.Error User)
init : (Model, Cmd Msg)
init =
( { user = Nothing }
, Cmd.none -- No command initially
)
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
FetchUser ->
( model
, Http.get
{ url = "/api/user"
, expect = Http.expectJson GotUser userDecoder
}
)
GotUser result ->
case result of
Ok user ->
( { model | user = Just user }
, Cmd.none
)
Err _ ->
( model
, Cmd.none
)
-- Combining commands
Cmd.batch : List (Cmd msg) -> Cmd msg
Cmd.batch
[ fetchUser
, fetchPosts
, logEvent
]
Subscriptions (Sub)
Subscriptions listen to external events:
import Browser.Events
import Time
type Msg
= Tick Time.Posix
| KeyPressed String
| MouseMoved Int Int
subscriptions : Model -> Sub Msg
subscriptions model =
if model.isActive then
Sub.batch
[ Time.every 1000 Tick
, Browser.Events.onKeyPress keyDecoder
, Browser.Events.onMouseMove mouseDecoder
]
else
Sub.none
-- Subscription functions
Time.every : Float -> (Time.Posix -> msg) -> Sub msg
Browser.Events.onKeyPress : Decode.Decoder msg -> Sub msg
Browser.Events.onAnimationFrame : (Time.Posix -> msg) -> Sub msg
View Functions
HTML Generation:
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
view : Model -> Html Msg
view model =
div [ class "container" ]
[ header [ class "header" ]
[ h1 [] [ text "My App" ]
]
, main_ [ class "content" ]
[ viewUserForm model.form
, viewUserList model.users
]
, footer [ class "footer" ]
[ text "© 2025" ]
]
-- Conditional rendering
viewIf : Bool -> Html msg -> Html msg
viewIf condition content =
if condition then
content
else
text ""
-- List rendering
viewUserList : List User -> Html Msg
viewUserList users =
div [ class "user-list" ]
[ h2 [] [ text "Users" ]
, ul [] (List.map viewUser users)
]
viewUser : User -> Html Msg
viewUser user =
li [ class "user-item" ]
[ text user.name
, button [ onClick (DeleteUser user.id) ]
[ text "Delete" ]
]
State Management Patterns
Nested Updates:
type alias Model =
{ loginForm : LoginForm
, settings : Settings
}
type Msg
= LoginFormMsg LoginFormMsg
| SettingsMsg SettingsMsg
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
LoginFormMsg formMsg ->
let
(newForm, formCmd) =
LoginForm.update formMsg model.loginForm
in
( { model | loginForm = newForm }
, Cmd.map LoginFormMsg formCmd
)
SettingsMsg settingsMsg ->
let
(newSettings, settingsCmd) =
Settings.update settingsMsg model.settings
in
( { model | settings = newSettings }
, Cmd.map SettingsMsg settingsCmd
)
HTTP and JSON
Making HTTP Requests
GET Requests:
import Http
import Json.Decode as Decode
type Msg
= FetchData
| GotData (Result Http.Error String)
fetchData : Cmd Msg
fetchData =
Http.get
{ url = "https://api.example.com/data"
, expect = Http.expectString GotData
}
-- With JSON decoder
type alias User =
{ id : Int
, name : String
}
fetchUser : Int -> Cmd Msg
fetchUser userId =
Http.get
{ url = "https://api.example.com/users/" ++ String.fromInt userId
, expect = Http.expectJson GotUser userDecoder
}
userDecoder : Decode.Decoder User
userDecoder =
Decode.map2 User
(Decode.field "id" Decode.int)
(Decode.field "name" Decode.string)
POST Requests:
import Json.Encode as Encode
type Msg
= CreateUser
| UserCreated (Result Http.Error User)
createUser : String -> String -> Cmd Msg
createUser name email =
Http.post
{ url = "https://api.example.com/users"
, body = Http.jsonBody (encodeUser name email)
, expect = Http.expectJson UserCreated userDecoder
}
encodeUser : String -> String -> Encode.Value
encodeUser name email =
Encode.object
[ ("name", Encode.string name)
, ("email", Encode.string email)
]
Custom Requests:
updateUser : Int -> User -> Cmd Msg
updateUser userId user =
Http.request
{ method = "PUT"
, headers = [ Http.header "Authorization" "Bearer token123" ]
, url = "https://api.example.com/users/" ++ String.fromInt userId
, body = Http.jsonBody (encodeUserUpdate user)
, expect = Http.expectJson UserUpdated userDecoder
, timeout = Just 10000
, tracker = Nothing
}
JSON Decoding
Basic Decoders:
import Json.Decode as Decode exposing (Decoder)
-- Primitive decoders
stringDecoder : Decoder String
stringDecoder = Decode.string
intDecoder : Decoder Int
intDecoder = Decode.int
floatDecoder : Decoder Float
floatDecoder = Decode.float
boolDecoder : Decoder Bool
boolDecoder = Decode.bool
-- Field extraction
nameDecoder : Decoder String
nameDecoder =
Decode.field "name" Decode.string
-- Nested fields
streetDecoder : Decoder String
streetDecoder =
Decode.at ["address", "street"] Decode.string
Complex Decoders:
type alias User =
{ id : Int
, name : String
, email : String
, age : Maybe Int
, isActive : Bool
}
userDecoder : Decoder User
userDecoder =
Decode.map5 User
(Decode.field "id" Decode.int)
(Decode.field "name" Decode.string)
(Decode.field "email" Decode.string)
(Decode.maybe (Decode.field "age" Decode.int))
(Decode.field "is_active" Decode.bool)
-- Alternative with pipeline style (requires elm-json-decode-pipeline)
userDecoderPipeline : Decoder User
userDecoderPipeline =
Decode.succeed User
|> required "id" Decode.int
|> required "name" Decode.string
|> required "email" Decode.string
|> optional "age" (Decode.maybe Decode.int) Nothing
|> required "is_active" Decode.bool
List and Array Decoders:
-- Decode list of users
usersDecoder : Decoder (List User)
usersDecoder =
Decode.list userDecoder
-- Decode field containing list
postsDecoder : Decoder (List Post)
postsDecoder =
Decode.field "posts" (Decode.list postDecoder)
-- Decode dictionary
userByIdDecoder : Decoder (Dict String User)
userByIdDecoder =
Decode.dict userDecoder
Handling Decode Errors:
type Msg
= GotData (Result Http.Error Data)
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
GotData result ->
case result of
Ok data ->
( { model | data = Just data }
, Cmd.none
)
Err httpError ->
case httpError of
Http.BadUrl url ->
( { model | error = Just ("Bad URL: " ++ url) }
, Cmd.none
)
Http.Timeout ->
( { model | error = Just "Request timeout" }
, Cmd.none
)
Http.NetworkError ->
( { model | error = Just "Network error" }
, Cmd.none
)
Http.BadStatus code ->
( { model | error = Just ("Bad status: " ++ String.fromInt code) }
, Cmd.none
)
Http.BadBody message ->
( { model | error = Just ("Decode error: " ++ message) }
, Cmd.none
)
JavaScript Interop
Flags (Initial Data)
Pass data from JavaScript to Elm on initialization:
Elm Side:
type alias Flags =
{ apiKey : String
, userId : Int
, theme : String
}
init : Flags -> (Model, Cmd Msg)
init flags =
( { apiKey = flags.apiKey
, userId = flags.userId
, theme = flags.theme
, data = Nothing
}
, fetchUserData flags.userId
)
main : Program Flags Model Msg
main =
Browser.element
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
}
JavaScript Side:
var app = Elm.Main.init({
node: document.getElementById('app'),
flags: {
apiKey: 'your-api-key',
userId: 12345,
theme: 'dark'
}
});
Ports (Bidirectional Communication)
Defining Ports:
port module Main exposing (..)
-- Port for sending data to JavaScript
port saveToLocalStorage : String -> Cmd msg
-- Port for receiving data from JavaScript
port onStorageChange : (String -> msg) -> Sub msg
-- Using ports in update
type Msg
= SaveData String
| StorageChanged String
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
SaveData data ->
( model
, saveToLocalStorage data
)
StorageChanged data ->
( { model | data = data }
, Cmd.none
)
-- Using ports in subscriptions
subscriptions : Model -> Sub Msg
subscriptions model =
onStorageChange StorageChanged
JavaScript Side:
var app = Elm.Main.init({
node: document.getElementById('app')
});
// Receiving from Elm
app.ports.saveToLocalStorage.subscribe(function(data) {
localStorage.setItem('app-data', data);
});
// Sending to Elm
window.addEventListener('storage', function(e) {
if (e.key === 'app-data') {
app.ports.onStorageChange.send(e.newValue);
}
});
Common Port Patterns:
-- Local storage
port setStorage : String -> Cmd msg
port getStorage : (String -> msg) -> Sub msg
-- WebSocket
port sendSocketMessage : String -> Cmd msg
port receiveSocketMessage : (String -> msg) -> Sub msg
-- Analytics
port trackEvent : { category : String, action : String } -> Cmd msg
-- Third-party libraries
port initMap : { lat : Float, lng : Float } -> Cmd msg
port updateMarker : { lat : Float, lng : Float } -> Cmd msg
Custom Elements
Embed Elm in existing applications using web components:
JavaScript Side:
customElements.define('elm-widget', class extends HTMLElement {
connectedCallback() {
this.app = Elm.Widget.init({
node: this,
flags: {
initialValue: this.getAttribute('value')
}
});
// Listen to Elm events
this.app.ports.valueChanged.subscribe(value => {
this.dispatchEvent(new CustomEvent('change', { detail: value }));
});
}
});
HTML Usage:
<elm-widget value="initial"></elm-widget>
Tooling and Build System
elm.json Configuration
Package vs Application:
{
"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": {
"elm-explorations/test": "1.2.2"
},
"indirect": {}
}
}
Common Elm Commands
Building:
# Compile to JavaScript
elm make src/Main.elm --output=main.js
# Compile with optimization
elm make src/Main.elm --output=main.js --optimize
# Compile multiple files
elm make src/Main.elm src/Admin.elm --output=dist/
# Debug build
elm make src/Main.elm --output=main.js --debug
Development:
# Start development server
elm reactor
# Install packages
elm install elm/http
elm install elm/json
elm install elm/random
# Format code
elm-format src/ --yes
# Run tests
elm-test
Optimization:
# Build optimized
elm make src/Main.elm --output=main.js --optimize
# Minify with uglify-js
uglifyjs main.js --compress 'pure_funcs=[F2,F3,F4,F5,F6,F7,F8,F9,A2,A3,A4,A5,A6,A7,A8,A9],pure_getters,keep_fargs=false,unsafe_comps,unsafe' | uglifyjs --mangle --output main.min.js
Project Structure
Recommended Structure:
my-elm-app/
├── elm.json # Package configuration
├── src/
│ ├── Main.elm # Entry point
│ ├── Models/
│ │ ├── User.elm
│ │ └── Post.elm
│ ├── Views/
│ │ ├── Home.elm
│ │ ├── Profile.elm
│ │ └── Common/
│ │ ├── Header.elm
│ │ └── Footer.elm
│ ├── Updates/
│ │ ├── User.elm
│ │ └── Post.elm
│ ├── Api/
│ │ ├── User.elm
│ │ └── Post.elm
│ ├── Utils/
│ │ ├── Validators.elm
│ │ └── Formatters.elm
│ └── Ports.elm
├── tests/
│ └── Tests.elm
├── public/
│ ├── index.html
│ ├── styles.css
│ └── assets/
└── README.md
Common Patterns
Form Handling
Complete Form Pattern:
type alias Form =
{ email : String
, password : String
, errors : List String
, isSubmitting : Bool
}
type FormMsg
= UpdateEmail String
| UpdatePassword String
| SubmitForm
| FormSubmitted (Result Http.Error User)
initForm : Form
initForm =
{ email = ""
, password = ""
, errors = []
, isSubmitting = False
}
updateForm : FormMsg -> Form -> (Form, Cmd FormMsg)
updateForm msg form =
case msg of
UpdateEmail email ->
( { form | email = email }
, Cmd.none
)
UpdatePassword password ->
( { form | password = password }
, Cmd.none
)
SubmitForm ->
case validateForm form of
Ok validForm ->
( { form | isSubmitting = True, errors = [] }
, submitForm validForm
)
Err errors ->
( { form | errors = errors }
, Cmd.none
)
FormSubmitted result ->
case result of
Ok user ->
( initForm
, Cmd.none
)
Err _ ->
( { form
| isSubmitting = False
, errors = ["Submission failed"]
}
, Cmd.none
)
validateForm : Form -> Result (List String) Form
validateForm form =
let
errors =
[ validateEmail form.email
, validatePassword form.password
]
|> List.filterMap identity
in
if List.isEmpty errors then
Ok form
else
Err errors
viewForm : Form -> Html FormMsg
viewForm form =
Html.form [ onSubmit SubmitForm ]
[ input
[ type_ "email"
, value form.email
, onInput UpdateEmail
, disabled form.isSubmitting
]
[]
, input
[ type_ "password"
, value form.password
, onInput UpdatePassword
, disabled form.isSubmitting
]
[]
, div [ class "errors" ]
(List.map viewError form.errors)
, button
[ type_ "submit"
, disabled form.isSubmitting
]
[ text (if form.isSubmitting then "Submitting..." else "Submit") ]
]
Routing (SPA Navigation)
URL Routing with Browser.application:
import Browser
import Browser.Navigation as Nav
import Url
import Url.Parser as Parser exposing (Parser, oneOf, s, int, (</>))
type Route
= Home
| Profile Int
| Settings
| NotFound
routeParser : Parser (Route -> a) a
routeParser =
oneOf
[ Parser.map Home Parser.top
, Parser.map Profile (s "profile" </> int)
, Parser.map Settings (s "settings")
]
fromUrl : Url.Url -> Route
fromUrl url =
Parser.parse routeParser url
|> Maybe.withDefault NotFound
type alias Model =
{ key : Nav.Key
, route : Route
}
type Msg
= LinkClicked Browser.UrlRequest
| UrlChanged Url.Url
init : () -> Url.Url -> Nav.Key -> (Model, Cmd Msg)
init _ url key =
( { key = key
, route = fromUrl url
}
, Cmd.none
)
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
)
view : Model -> Browser.Document Msg
view model =
{ title = "My App"
, body =
[ viewNavigation
, viewRoute model.route
]
}
viewRoute : Route -> Html Msg
viewRoute route =
case route of
Home ->
viewHome
Profile userId ->
viewProfile userId
Settings ->
viewSettings
NotFound ->
div [] [ text "404 Not Found" ]
main : Program () Model Msg
main =
Browser.application
{ init = init
, view = view
, update = update
, subscriptions = \_ -> Sub.none
, onUrlChange = UrlChanged
, onUrlRequest = LinkClicked
}
Debouncing User Input
Search with Debounce:
type alias Model =
{ searchQuery : String
, searchDebounce : Int
, results : List Result
}
type Msg
= UpdateSearch String
| DebounceTick Time.Posix
| PerformSearch
| GotResults (Result Http.Error (List Result))
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
UpdateSearch query ->
( { model
| searchQuery = query
, searchDebounce = 3 -- Wait 3 ticks
}
, Cmd.none
)
DebounceTick _ ->
if model.searchDebounce > 0 then
( { model | searchDebounce = model.searchDebounce - 1 }
, Cmd.none
)
else if model.searchQuery /= "" then
update PerformSearch model
else
( model, Cmd.none )
PerformSearch ->
( model
, searchApi model.searchQuery
)
GotResults result ->
case result of
Ok results ->
( { model | results = results }
, Cmd.none
)
Err _ ->
( model, Cmd.none )
subscriptions : Model -> Sub Msg
subscriptions model =
Time.every 300 DebounceTick -- Check every 300ms
Loading States
Remote Data Pattern:
type RemoteData error value
= NotAsked
| Loading
| Success value
| Failure error
type alias Model =
{ userData : RemoteData Http.Error User
}
type Msg
= FetchUser
| GotUser (Result Http.Error User)
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
FetchUser ->
( { model | userData = Loading }
, fetchUser
)
GotUser result ->
case result of
Ok user ->
( { model | userData = Success user }
, Cmd.none
)
Err error ->
( { model | userData = Failure error }
, Cmd.none
)
viewUser : RemoteData Http.Error User -> Html Msg
viewUser userData =
case userData of
NotAsked ->
button [ onClick FetchUser ] [ text "Load User" ]
Loading ->
div [ class "spinner" ] [ text "Loading..." ]
Success user ->
div [ class "user" ]
[ h2 [] [ text user.name ]
, p [] [ text user.email ]
]
Failure error ->
div [ class "error" ]
[ text "Failed to load user"
, button [ onClick FetchUser ] [ text "Retry" ]
]
Best Practices
Code Organization
- Keep Modules Small: Each module should have a single responsibility
- Use Type Aliases: Document complex types with meaningful names
- Expose Minimally: Only expose what's needed from modules
- Group Related Code: Keep related types, functions, and views together
- Consistent Naming: Use clear, consistent naming conventions
Type Safety
- Always Add Type Annotations: For top-level functions
- Use Custom Types: Instead of strings/ints for states
- Make Impossible States Impossible: Design types to prevent invalid states
- Leverage Maybe and Result: Never use defaults that hide errors
- Pattern Match Exhaustively: Cover all cases explicitly
Performance
- Use Html.Lazy: For expensive view functions that rarely change
- Optimize List Operations: Consider using Array for random access
- Batch Commands: Use Cmd.batch for multiple effects
- Minimize Subscriptions: Only subscribe when needed
- Profile Before Optimizing: Use elm reactor debugger
Testing
import Test exposing (..)
import Expect
suite : Test
suite =
describe "User Validation"
[ test "validates correct email" <|
\_ ->
validateEmail "test@example.com"
|> Expect.equal (Ok "test@example.com")
, test "rejects invalid email" <|
\_ ->
validateEmail "invalid"
|> Expect.err
, describe "Password validation"
[ test "requires minimum length" <|
\_ ->
validatePassword "short"
|> Expect.err
, test "accepts valid password" <|
\_ ->
validatePassword "securePassword123"
|> Expect.ok
]
]
Error Handling
- Use Result for Validation: Return meaningful error messages
- Handle All Http.Error Cases: Provide user-friendly messages
- Display Errors Clearly: Show validation errors next to form fields
- Provide Recovery Options: Offer retry buttons, alternative actions
- Log Errors via Ports: Send to external logging services
Accessibility
-- Use semantic HTML
viewButton : String -> msg -> Html msg
viewButton label msg =
button
[ onClick msg
, ariaLabel label
]
[ text label ]
-- Provide keyboard navigation
viewModal : Html msg
viewModal =
div
[ role "dialog"
, ariaModal True
, tabindex -1
]
[ -- modal content
]
-- Use ARIA attributes
viewStatus : String -> Html msg
viewStatus message =
div
[ ariaLive "polite"
, ariaAtomic True
]
[ text message ]
Resources and Learning
Official Resources:
- Elm Guide: https://guide.elm-lang.org
- Package Documentation: https://package.elm-lang.org
- Elm Discourse: https://discourse.elm-lang.org
- Elm Slack: https://elmlang.herokuapp.com
Key Packages:
- elm/browser - Browser-based programs
- elm/html - HTML generation
- elm/http - HTTP requests
- elm/json - JSON encoding/decoding
- elm/random - Random value generation
- elm/time - Time and dates
- elm/url - URL parsing and building
- elm-explorations/test - Testing framework
Learning Resources:
- Elm in Action (book)
- Elm Radio (podcast)
- Elm Weekly (newsletter)
- Exercism Elm Track
Skill Version: 1.0.0 Last Updated: November 2025 Skill Category: Functional Programming, Web Development, Type-Safe Programming Compatible With: JavaScript, Web Components, SPA Frameworks