elm-development
Comprehensive Elm functional programming for web development including The Elm Architecture, type system, JavaScript interop, and production-ready patterns
- Category: discovered
- Tags: github, community
- Version: 1.0.0
- Author: walid
- Downloads: 0
- Updated: 2025-11-22
Download the latest version (v1.0.0) from this page.
SKILL.md
---
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:**
1. **Model**: The complete state of your application
2. **View**: A pure function that renders the model as HTML
3. **Update**: A pure function that updates the model based on messages
**Data Flow:**
```
Model → View → HTML
↑ ↓
└── Update ←──┘
Messages
```
**Basic Pattern:**
```elm
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 effects
- `Browser.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:**
```elm
-- 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:
```elm
-- 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:**
```elm
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:**
```elm
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:
```elm
-- 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:**
```elm
-- 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:**
```elm
calculateArea : Float -> Float -> Float
calculateArea width height =
let
perimeter =
2 * (width + height)
area =
width * height
in
area
```
### Records
**Record Syntax:**
```elm
-- 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):**
```elm
-- 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):**
```elm
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:**
```elm
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:**
```elm
-- 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:
```elm
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:
```elm
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:**
```elm
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:**
```elm
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:**
```elm
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:**
```elm
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:**
```elm
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:**
```elm
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:**
```elm
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:**
```elm
-- 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:**
```elm
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:**
```elm
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:**
```javascript
var app = Elm.Main.init({
node: document.getElementById('app'),
flags: {
apiKey: 'your-api-key',
userId: 12345,
theme: 'dark'
}
});
```
### Ports (Bidirectional Communication)
**Defining Ports:**
```elm
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:**
```javascript
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:**
```elm
-- Local storage
port setStor