Claude-skill-registry elm-development
Comprehensive Elm functional programming for web development including The Elm Architecture, type system, JavaScript interop, and production-ready patterns
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/elm-development" ~/.claude/skills/majiayu000-claude-skill-registry-elm-development && rm -rf "$T"
skills/data/elm-development/SKILL.mdElm 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
- initial state plus commands to run(Model, Cmd Msg) - update: Returns
- new state plus commands to run(Model, Cmd Msg) - subscriptions: Listens to external events (time, websockets, etc.)
- view: Same pure function rendering HTML
Program Types:
: Simple apps without side effectsBrowser.sandbox
: Apps with side effects (HTTP, etc.)Browser.element
: Control overBrowser.document
and<title><body>
: Full SPA with URL routingBrowser.application
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