NexusCS

Elm

Functional programming
Elm is a functional programming language for building reliable web applications with no runtime exceptions. Features the Elm Architecture (Model-Update-View), strong static typing, and friendly compiler errors.
functional
web
frontend

Getting started

Introduction

Elm is a purely functional language that compiles to JavaScript. It guarantees no runtime exceptions and provides a delightful development experience with helpful compiler messages.

Installation

# Via npm
npm install -g elm

# Via Homebrew (macOS)
brew install elm

# Verify installation
elm --version

Quick Start

# Initialize project
elm init

# Compile to JavaScript
elm make src/Main.elm --output=main.js

# Compile with optimizations
elm make src/Main.elm --optimize --output=main.js

# Start development server
elm reactor

# Live reload development
elm-live src/Main.elm --open --output=main.js

Hello World

module Main exposing (main)

import Html exposing (text)

main =
    text "Hello, World!"

The Elm Architecture

Model-Update-View Pattern

The core pattern for all Elm applications.

-- MODEL
type alias Model =
    { count : Int
    , message : String
    }

init : Model
init =
    { count = 0
    , message = "Click the button!"
    }

-- UPDATE
type Msg
    = Increment
    | Decrement
    | Reset

update : Msg -> Model -> Model
update msg model =
    case msg of
        Increment ->
            { model | count = model.count + 1 }

        Decrement ->
            { model | count = model.count - 1 }

        Reset ->
            { model | count = 0 }

-- VIEW
view : Model -> Html Msg
view model =
    div []
        [ button [ onClick Decrement ] [ text "-" ]
        , span [] [ text (String.fromInt model.count) ]
        , button [ onClick Increment ] [ text "+" ]
        , button [ onClick Reset ] [ text "Reset" ]
        ]

With Commands

For side effects (HTTP, random, etc.)

import Http
import Json.Decode as Decode

type Msg
    = GotData (Result Http.Error String)
    | FetchData

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    case msg of
        FetchData ->
            ( { model | loading = True }
            , Http.get
                { url = "https://api.example.com/data"
                , expect = Http.expectString GotData
                }
            )

        GotData result ->
            case result of
                Ok data ->
                    ( { model | data = data, loading = False }
                    , Cmd.none
                    )

                Err _ ->
                    ( { model | error = "Failed", loading = False }
                    , Cmd.none
                    )

With Subscriptions

For listening to external events.

import Browser.Events
import Time

type Msg
    = Tick Time.Posix
    | KeyPressed String

subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.batch
        [ Time.every 1000 Tick
        , Browser.Events.onKeyPress keyDecoder
        ]

keyDecoder : Decode.Decoder Msg
keyDecoder =
    Decode.map KeyPressed
        (Decode.field "key" Decode.string)

Types

Basic Types

-- Primitives
number : Int
number = 42

decimal : Float
decimal = 3.14

text : String
text = "Hello"

flag : Bool
flag = True

-- Lists (homogeneous)
numbers : List Int
numbers = [1, 2, 3, 4]

-- Tuples (heterogeneous)
pair : (String, Int)
pair = ("Alice", 25)

triple : (Int, String, Bool)
triple = (1, "two", True)

Type Aliases

-- Record type alias
type alias User =
    { name : String
    , age : Int
    , email : String
    }

-- Usage
user : User
user =
    { name = "Alice"
    , age = 30
    , email = "alice@example.com"
    }

-- Update syntax
olderUser : User
olderUser =
    { user | age = 31 }

Custom Types (Union Types)

-- Simple enum
type Status
    = Loading
    | Success
    | Failure

-- With associated data
type User
    = Anonymous
    | Registered String Int
    | Admin String String

-- Recursive types
type Tree a
    = Empty
    | Node a (Tree a) (Tree a)

-- Generic Result-like type
type Response a
    = Loading
    | Success a
    | Error String

Maybe Type

For values that might not exist.

-- Definition
type Maybe a
    = Just a
    | Nothing

-- Usage
findUser : Int -> Maybe User
findUser id =
    if id > 0 then
        Just { name = "Alice", age = 30 }
    else
        Nothing

-- Pattern matching
displayAge : Maybe Int -> String
displayAge maybeAge =
    case maybeAge of
        Just age ->
            "Age: " ++ String.fromInt age

        Nothing ->
            "Age unknown"

-- Using Maybe.map
incrementAge : Maybe Int -> Maybe Int
incrementAge =
    Maybe.map (\age -> age + 1)

-- Maybe.withDefault
getAge : Maybe Int -> Int
getAge maybeAge =
    Maybe.withDefault 0 maybeAge

Result Type

For operations that can fail.

-- Definition
type Result error value
    = Ok value
    | Err error

-- Usage
divide : Float -> Float -> Result String Float
divide a b =
    if b == 0 then
        Err "Cannot divide by zero"
    else
        Ok (a / b)

-- Pattern matching
displayResult : Result String Int -> String
displayResult result =
    case result of
        Ok value ->
            "Success: " ++ String.fromInt value

        Err error ->
            "Error: " ++ error

-- Chaining with Result.andThen
parseAndValidate : String -> Result String Int
parseAndValidate input =
    String.toInt input
        |> Result.fromMaybe "Not a number"
        |> Result.andThen validatePositive

validatePositive : Int -> Result String Int
validatePositive n =
    if n > 0 then
        Ok n
    else
        Err "Must be positive"

Pattern Matching

Case Expressions

-- Basic pattern matching
describe : Int -> String
describe n =
    case n of
        0 ->
            "zero"

        1 ->
            "one"

        _ ->
            "many"

-- Matching custom types
statusMessage : Status -> String
statusMessage status =
    case status of
        Loading ->
            "Loading..."

        Success ->
            "Done!"

        Failure ->
            "Error occurred"

Destructuring

-- Lists
first : List a -> Maybe a
first list =
    case list of
        [] ->
            Nothing

        head :: tail ->
            Just head

-- Tuples
addPair : (Int, Int) -> Int
addPair pair =
    case pair of
        (x, y) ->
            x + y

-- Records
getName : User -> String
getName user =
    case user of
        { name } ->
            name

-- Alternative record syntax
getEmail : User -> String
getEmail { email } =
    email

Nested Patterns

-- Complex pattern matching
describe : Maybe (List Int) -> String
describe value =
    case value of
        Nothing ->
            "No list"

        Just [] ->
            "Empty list"

        Just [x] ->
            "One item: " ++ String.fromInt x

        Just (x :: y :: rest) ->
            "Multiple items starting with "
                ++ String.fromInt x

-- Using as pattern
transform : Maybe User -> String
transform maybeUser =
    case maybeUser of
        Just ({ name, age } as user) ->
            name ++ " is " ++ String.fromInt age

        Nothing ->
            "No user"

Functions

Function Syntax

-- Simple function
add : Int -> Int -> Int
add x y =
    x + y

-- Anonymous function
increment : Int -> Int
increment =
    \n -> n + 1

-- Partial application
add5 : Int -> Int
add5 =
    add 5

-- Multiple arguments
greet : String -> String -> String
greet greeting name =
    greeting ++ ", " ++ name ++ "!"

Pipeline Operator

-- Forward pipe (|>)
result : Int
result =
    [1, 2, 3, 4]
        |> List.map (\x -> x * 2)
        |> List.filter (\x -> x > 4)
        |> List.sum
    -- Result: 14

-- Backward pipe (<|)
result : String
result =
    String.toUpper <|
        String.trim <|
            "  hello  "

Composition

-- Function composition (>>)
processString : String -> String
processString =
    String.trim
        >> String.toLower
        >> String.reverse

-- Backward composition (<<)
validate : String -> Result String Int
validate =
    String.toInt
        << String.trim

Let Expressions

distance : Float -> Float -> Float -> Float
distance x1 y1 x2 y2 =
    let
        dx =
            x2 - x1

        dy =
            y2 - y1

        squaredDistance =
            dx * dx + dy * dy
    in
    sqrt squaredDistance

HTML and Views

Basic HTML

import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)

-- Simple elements
view : Html msg
view =
    div []
        [ h1 [] [ text "Title" ]
        , p [] [ text "Paragraph" ]
        , span [] [ text "Inline" ]
        ]

-- With attributes
styledView : Html msg
styledView =
    div [ class "container", id "main" ]
        [ h1 [ class "title" ] [ text "Hello" ]
        , p [ style "color" "blue" ] [ text "Text" ]
        ]

Event Handlers

-- Click events
button : Html Msg
button =
    Html.button
        [ onClick Increment ]
        [ text "Click me" ]

-- Input events
inputField : Html Msg
inputField =
    input
        [ type_ "text"
        , placeholder "Enter text"
        , value model.inputValue
        , onInput UpdateInput
        ]
        []

-- Custom event decoder
onEnter : msg -> Attribute msg
onEnter msg =
    on "keypress" <|
        Decode.andThen
            (\key ->
                if key == 13 then
                    Decode.succeed msg
                else
                    Decode.fail "Not Enter"
            )
            keyCode

Lists and Conditionals

-- Rendering lists
viewUsers : List User -> Html msg
viewUsers users =
    ul [] (List.map viewUser users)

viewUser : User -> Html msg
viewUser user =
    li [] [ text user.name ]

-- Conditional rendering
viewStatus : Bool -> Html msg
viewStatus isLoading =
    if isLoading then
        div [] [ text "Loading..." ]
    else
        div [] [ text "Ready!" ]

-- Using case for multiple conditions
viewState : Status -> Html msg
viewState status =
    case status of
        Loading ->
            spinner

        Success ->
            successMessage

        Failure ->
            errorMessage

JSON Decoders

Basic Decoders

import Json.Decode as Decode exposing (Decoder)

-- Primitive decoders
stringDecoder : Decoder String
stringDecoder =
    Decode.string

intDecoder : Decoder Int
intDecoder =
    Decode.int

boolDecoder : Decoder Bool
boolDecoder =
    Decode.bool

-- Field decoder
nameDecoder : Decoder String
nameDecoder =
    Decode.field "name" Decode.string

Object Decoders

-- Simple record decoder
type alias User =
    { name : String
    , age : Int
    , email : String
    }

userDecoder : Decoder User
userDecoder =
    Decode.map3 User
        (Decode.field "name" Decode.string)
        (Decode.field "age" Decode.int)
        (Decode.field "email" Decode.string)

-- Alternative with pipeline style
import Json.Decode.Pipeline exposing (required, optional)

userDecoderPipeline : Decoder User
userDecoderPipeline =
    Decode.succeed User
        |> required "name" Decode.string
        |> required "age" Decode.int
        |> optional "email" Decode.string "no-email"

Complex Decoders

-- List decoder
usersDecoder : Decoder (List User)
usersDecoder =
    Decode.list userDecoder

-- Nested objects
type alias Post =
    { title : String
    , author : User
    , tags : List String
    }

postDecoder : Decoder Post
postDecoder =
    Decode.map3 Post
        (Decode.field "title" Decode.string)
        (Decode.field "author" userDecoder)
        (Decode.field "tags" (Decode.list Decode.string))

-- Optional fields
type alias Profile =
    { name : String
    , bio : Maybe String
    }

profileDecoder : Decoder Profile
profileDecoder =
    Decode.map2 Profile
        (Decode.field "name" Decode.string)
        (Decode.maybe (Decode.field "bio" Decode.string))

-- Custom decoders
statusDecoder : Decoder Status
statusDecoder =
    Decode.string
        |> Decode.andThen
            (\str ->
                case str of
                    "loading" ->
                        Decode.succeed Loading

                    "success" ->
                        Decode.succeed Success

                    "failure" ->
                        Decode.succeed Failure

                    _ ->
                        Decode.fail "Unknown status"
            )

Using Decoders

-- Decoding JSON strings
parseUser : String -> Result Decode.Error User
parseUser jsonString =
    Decode.decodeString userDecoder jsonString

-- With HTTP
fetchUser : Cmd Msg
fetchUser =
    Http.get
        { url = "https://api.example.com/user"
        , expect = Http.expectJson GotUser userDecoder
        }

type Msg
    = GotUser (Result Http.Error User)

Commands and HTTP

HTTP Requests

import Http
import Json.Decode as Decode

-- GET request
fetchData : Cmd Msg
fetchData =
    Http.get
        { url = "https://api.example.com/data"
        , expect = Http.expectJson GotData dataDecoder
        }

-- POST request
postData : User -> Cmd Msg
postData user =
    Http.post
        { url = "https://api.example.com/users"
        , body = Http.jsonBody (encodeUser user)
        , expect = Http.expectJson UserCreated userDecoder
        }

-- Custom request
updateUser : Int -> User -> Cmd Msg
updateUser id user =
    Http.request
        { method = "PUT"
        , headers = [ Http.header "Authorization" "Bearer token" ]
        , url = "https://api.example.com/users/" ++ String.fromInt id
        , body = Http.jsonBody (encodeUser user)
        , expect = Http.expectJson UserUpdated userDecoder
        , timeout = Nothing
        , tracker = Nothing
        }

Command Batching

-- Batch multiple commands
init : (Model, Cmd Msg)
init =
    ( initialModel
    , Cmd.batch
        [ fetchUsers
        , fetchPosts
        , fetchComments
        ]
    )

-- No command
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    case msg of
        SomeMsg ->
            ( newModel, Cmd.none )

Random Values

import Random

-- Generate random integer
generateDiceRoll : Cmd Msg
generateDiceRoll =
    Random.generate GotDiceRoll (Random.int 1 6)

type Msg
    = GotDiceRoll Int

-- Generate multiple random values
type alias Point =
    { x : Float, y : Float }

randomPoint : Random.Generator Point
randomPoint =
    Random.map2 Point
        (Random.float 0 100)
        (Random.float 0 100)

generatePoint : Cmd Msg
generatePoint =
    Random.generate GotPoint randomPoint

Lists and Common Functions

List Operations

-- Creating lists
list1 : List Int
list1 = [1, 2, 3]

list2 : List Int
list2 = 1 :: 2 :: 3 :: []

range : List Int
range = List.range 1 10

-- Common functions
List.length [1, 2, 3]        -- 3
List.isEmpty []              -- True
List.head [1, 2, 3]          -- Just 1
List.tail [1, 2, 3]          -- Just [2, 3]
List.take 2 [1, 2, 3, 4]     -- [1, 2]
List.drop 2 [1, 2, 3, 4]     -- [3, 4]
List.reverse [1, 2, 3]       -- [3, 2, 1]
List.member 2 [1, 2, 3]      -- True

List Transformations

-- Map
List.map (\x -> x * 2) [1, 2, 3]
-- [2, 4, 6]

-- Filter
List.filter (\x -> x > 2) [1, 2, 3, 4]
-- [3, 4]

-- FilterMap
List.filterMap String.toInt ["1", "a", "3"]
-- [1, 3]

-- Fold (reduce)
List.foldl (+) 0 [1, 2, 3, 4]
-- 10

List.foldr (::) [] [1, 2, 3]
-- [1, 2, 3]

-- Concat
List.concat [[1, 2], [3, 4]]
-- [1, 2, 3, 4]

-- ConcatMap (flatMap)
List.concatMap (\x -> [x, x * 10]) [1, 2, 3]
-- [1, 10, 2, 20, 3, 30]

String Operations

-- Common functions
String.length "hello"           -- 5
String.isEmpty ""               -- True
String.reverse "hello"          -- "olleh"
String.toUpper "hello"          -- "HELLO"
String.toLower "HELLO"          -- "hello"
String.trim "  hello  "         -- "hello"

-- Combining
String.append "Hello" " World"  -- "Hello World"
String.concat ["a", "b", "c"]   -- "abc"
String.join ", " ["a", "b"]     -- "a, b"

-- Splitting
String.split "," "a,b,c"        -- ["a", "b", "c"]
String.words "hello world"      -- ["hello", "world"]
String.lines "a\nb\nc"          -- ["a", "b", "c"]

-- Checking
String.startsWith "He" "Hello"  -- True
String.endsWith "lo" "Hello"    -- True
String.contains "ell" "Hello"   -- True

-- Conversion
String.fromInt 42               -- "42"
String.toInt "42"               -- Just 42
String.fromFloat 3.14           -- "3.14"
String.toFloat "3.14"           -- Just 3.14

Modules and Imports

Module Definition

-- Exposing specific items
module Main exposing (main, Model, Msg)

-- Exposing everything (not recommended)
module Utils exposing (..)

-- Exposing types with constructors
module Types exposing (User(..), Status(..))

-- Exposing types without constructors
module Types exposing (User, Status)

Importing Modules

-- Basic import
import Html

-- Exposing specific items
import Html exposing (div, text)

-- Exposing all
import Html.Attributes exposing (..)

-- Qualified import
import Json.Decode as Decode
import Json.Encode as Encode

-- Multiple imports
import Html exposing (Html, div)
import Html.Attributes exposing (class, id)
import Html.Events exposing (onClick)

Module Organization

-- src/Main.elm
module Main exposing (main)

import Browser
import Model exposing (Model, init)
import Update exposing (Msg, update)
import View exposing (view)

main : Program () Model Msg
main =
    Browser.sandbox
        { init = init
        , update = update
        , view = view
        }

-- src/Model.elm
module Model exposing (Model, init)

-- src/Update.elm
module Update exposing (Msg(..), update)

-- src/View.elm
module View exposing (view)

Compilation and Development

Compilation Commands

# Compile single file to JavaScript
elm make src/Main.elm --output=main.js

# Compile with optimizations
elm make src/Main.elm --optimize --output=main.js

# Compile to HTML (includes runtime)
elm make src/Main.elm --output=index.html

# Debug mode (default)
elm make src/Main.elm --debug --output=main.js

Development Server

# Start Elm Reactor (localhost:8000)
elm reactor

# Navigate to:
# http://localhost:8000/src/Main.elm

Live Reload Development

# Install elm-live
npm install -g elm-live

# Start with live reload
elm-live src/Main.elm --open --output=main.js

# With custom port
elm-live src/Main.elm --port=8080 --output=main.js

# With hot reloading
elm-live src/Main.elm --hot --output=main.js

Project Management

# Initialize new project
elm init

# Install package
elm install elm/http
elm install elm/json

# Update dependencies
elm bump

# Validate elm.json
elm make src/Main.elm --output=/dev/null

elm.json Structure

{
  "type": "application",
  "source-directories": ["src"],
  "elm-version": "0.19.1",
  "dependencies": {
    "direct": {
      "elm/browser": "1.0.2",
      "elm/core": "1.0.5",
      "elm/html": "1.0.0",
      "elm/http": "2.0.0",
      "elm/json": "1.1.3"
    },
    "indirect": {
      "elm/bytes": "1.0.8",
      "elm/file": "1.0.5",
      "elm/time": "1.0.0",
      "elm/url": "1.0.0",
      "elm/virtual-dom": "1.0.3"
    }
  },
  "test-dependencies": {
    "direct": {},
    "indirect": {}
  }
}

Program Types

Browser.sandbox

For simple apps without side effects.

import Browser

main : Program () Model Msg
main =
    Browser.sandbox
        { init = init
        , update = update
        , view = view
        }

-- No Cmd or Sub
update : Msg -> Model -> Model

Browser.element

For apps with commands/subscriptions.

import Browser

main : Program () Model Msg
main =
    Browser.element
        { init = init
        , update = update
        , view = view
        , subscriptions = subscriptions
        }

init : (Model, Cmd Msg)
update : Msg -> Model -> (Model, Cmd Msg)
view : Model -> Html Msg
subscriptions : Model -> Sub Msg

Browser.document

For apps controlling <head> and <body>.

import Browser

main : Program () Model Msg
main =
    Browser.document
        { init = init
        , update = update
        , view = view
        , subscriptions = subscriptions
        }

view : Model -> Browser.Document Msg
view model =
    { title = "Page Title"
    , body =
        [ div [] [ text "Content" ]
        ]
    }

Browser.application

For single-page apps with URL routing.

import Browser
import Browser.Navigation as Nav
import Url exposing (Url)

main : Program () Model Msg
main =
    Browser.application
        { init = init
        , update = update
        , view = view
        , subscriptions = subscriptions
        , onUrlRequest = LinkClicked
        , onUrlChange = UrlChanged
        }

type Msg
    = LinkClicked Browser.UrlRequest
    | UrlChanged Url

init : () -> Url -> Nav.Key -> (Model, Cmd Msg)

Common Gotchas

Type Annotations Required

-- Error: missing type annotation
add x y =
    x + y

-- Fixed
add : Int -> Int -> Int
add x y =
    x + y

Record Update Syntax

-- Error: cannot reassign fields
user.age = 31

-- Fixed: use update syntax
{ user | age = 31 }

If/Then/Else Required

-- Error: missing else branch
if x > 0 then
    "positive"

-- Fixed: else is required
if x > 0 then
    "positive"
else
    "not positive"

Comparing Functions

-- Error: cannot compare functions with ==
(\x -> x + 1) == (\x -> x + 1)

-- Use named functions or refactor logic

String Concatenation

-- Error: cannot use + for strings
"Hello" + " World"

-- Fixed: use ++
"Hello" ++ " World"

-- Or use String.concat
String.concat ["Hello", " ", "World"]

Partial Application in Pipelines

-- Error: wrong argument order
users
    |> List.filter isActive

-- Fixed: lambda wrapper
users
    |> List.filter (\user -> isActive user)

-- Or use function composition
users
    |> List.filter isActive

Importing Constructors

-- Won't compile: Maybe not in scope
case value of
    Just x -> ...
    Nothing -> ...

-- Fixed: import with constructors
import Maybe exposing (Maybe(..))

-- Or use qualified names
case value of
    Maybe.Just x -> ...
    Maybe.Nothing -> ...

Best Practices

Type Safety First

Always use explicit type annotations for top-level functions. Let the compiler catch errors early.

-- Good
getUserName : User -> String
getUserName user =
    user.name

-- Avoid (inference works but less clear)
getUserName user =
    user.name

Small Update Functions

Break large update functions into smaller helpers.

-- Good
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    case msg of
        UserMsg userMsg ->
            updateUser userMsg model

        PostMsg postMsg ->
            updatePost postMsg model

-- Avoid giant case expressions

Use Custom Types

Prefer custom types over strings/booleans for state.

-- Good
type Status = Loading | Success | Failure

-- Avoid
type alias Model = { status : String }

Decode with Pipelines

Use Json.Decode.Pipeline for cleaner decoders.

import Json.Decode.Pipeline as Pipeline

userDecoder : Decoder User
userDecoder =
    Decode.succeed User
        |> Pipeline.required "name" Decode.string
        |> Pipeline.required "age" Decode.int
        |> Pipeline.optional "email" Decode.string ""

Also see