Elm
Web apps with no runtime errors.
Elm is a functional language that compiles to JavaScript for building web front ends. Its compiler is famous for friendly error messages, and well-written Elm apps have practically no runtime exceptions. The Elm Architecture (Model, Update, View) inspired Redux and many modern UI frameworks.
Why learn Elm?
Elm is the gentlest way to learn typed functional programming, and it shows how predictable UI code can be.
- No runtime exceptions: the compiler handles null and missing cases.
- Helpful compiler: error messages explain how to fix problems.
- The Elm Architecture: a clear pattern that influenced Redux.
Set up and run your first program
Install Elm from elm-lang.org or with npm. elm reactor serves files for quick experiments.
npm install -g elm
elm init
elm reactor # open http://localhost:8000module Main exposing (main)
import Html exposing (text)
name = "Ada"
main =
text ("Hello, AI! Welcome to Elm, " ++ name ++ ".")++ to join strings. Indentation matters.Values and functions
Values never change. Functions are defined by name and parameters with no parentheses or commas, and type annotations document them.
module Basics exposing (..)
layers : Int
layers = 32
fp16Gb : Float -> Float
fp16Gb params =
params * 2 / 1.0e9
params : Int -> Int -> Int
params l w =
12 * l * w * w
-- fp16Gb 8.03e9 == 16.06Custom types and case
Custom types model every possible state. case must handle all of them, which is why Elm apps do not crash on unexpected data.
type Device
= Cpu
| Gpu String Int
| Tpu Int
describe : Device -> String
describe device =
case device of
Cpu -> "CPU"
Gpu name vram -> name ++ " (" ++ String.fromInt vram ++ " GB)"
Tpu v -> "TPU v" ++ String.fromInt vMaybe, lists and records
There is no null: Maybe is Just value or Nothing. Records hold named fields, and the |> pipe chains list functions.
type alias Gpu = { name : String, vram : Int }
gpus : List Gpu
gpus = [ { name = "H100", vram = 80 }, { name = "H200", vram = 141 }, { name = "RTX 5090", vram = 32 } ]
bigNames : List String
bigNames =
gpus |> List.filter (\g -> g.vram >= 80) |> List.map .name
largest : Maybe Gpu
largest =
gpus |> List.sortBy .vram |> List.reverse |> List.headThe Elm Architecture
Every Elm app has a Model (state), an update function (how messages change state) and a view (HTML from state).
module Main exposing (main)
import Browser
import Html exposing (button, div, text)
import Html.Events exposing (onClick)
type alias Model = Int
type Msg = Increment | Decrement
update : Msg -> Model -> Model
update msg model =
case msg of
Increment -> model + 1
Decrement -> model - 1
view model =
div []
[ button [ onClick Decrement ] [ text "-" ]
, text (" Epochs: " ++ String.fromInt model ++ " ")
, button [ onClick Increment ] [ text "+" ]
]
main = Browser.sandbox { init = 0, update = update, view = view }HTTP and JSON decoders
Side effects are commands (Cmd). JSON is read with decoders that describe the shape you expect, so bad data becomes an error you handle, not a crash.
import Http
import Json.Decode as D
type Msg = GotStars (Result Http.Error Int)
starsDecoder : D.Decoder Int
starsDecoder =
D.field "stargazers_count" D.int
getStars : Cmd Msg
getStars =
Http.get
{ url = "https://api.github.com/repos/pytorch/pytorch"
, expect = Http.expectJson GotStars starsDecoder
}AI with Elm
Elm does not train models; it builds reliable interfaces for them. Chat front ends, dashboards for training metrics and labelling tools benefit from Elm's guarantee that every response shape and error case is handled. Elm talks to LLM APIs with Http, or to JavaScript AI libraries (such as Transformers.js) through ports.
The essential libraries
Decode an LLM response safely
A decoder for the OpenAI-compatible chat format; if the server returns something unexpected, you get an error value instead of a crash.
import Json.Decode as D
type alias ToolCall = { id : String, name : String, arguments : String }
type alias Message = { content : Maybe String, toolCalls : List ToolCall }
toolCall : D.Decoder ToolCall
toolCall =
D.map3 ToolCall
(D.field "id" D.string)
(D.at [ "function", "name" ] D.string)
(D.at [ "function", "arguments" ] D.string)
message : D.Decoder Message
message =
D.at [ "choices", "0", "message" ]
(D.map2 Message
(D.maybe (D.field "content" D.string))
(D.oneOf [ D.field "tool_calls" (D.list toolCall), D.succeed [] ]))A chat UI for a local LLM
This Elm app sends the conversation to a local model and shows replies. Adding tools means handling toolCalls in update and sending results back with another request.
module Main exposing (main)
import Browser
import Html exposing (..)
import Html.Attributes exposing (value)
import Html.Events exposing (onClick, onInput)
import Http
import Json.Decode as D
import Json.Encode as E
type alias Model = { input : String, log : List ( String, String ) }
type Msg = Typed String | Send | Got (Result Http.Error String)
encodeMsgs : List ( String, String ) -> E.Value
encodeMsgs log =
E.list (\( r, c ) -> E.object [ ( "role", E.string r ), ( "content", E.string c ) ]) log
ask : List ( String, String ) -> Cmd Msg
ask log =
Http.post
{ url = "http://localhost:11434/v1/chat/completions"
, body = Http.jsonBody (E.object [ ( "model", E.string "llama3.1" ), ( "messages", encodeMsgs log ) ])
, expect = Http.expectJson Got (D.at [ "choices", "0", "message", "content" ] D.string)
}
update : Msg -> Model -> ( Model, Cmd Msg )
update msg m =
case msg of
Typed s -> ( { m | input = s }, Cmd.none )
Send ->
let log = m.log ++ [ ( "user", m.input ) ] in
( { m | input = "", log = log }, ask log )
Got (Ok reply) -> ( { m | log = m.log ++ [ ( "assistant", reply ) ] }, Cmd.none )
Got (Err _) -> ( { m | log = m.log ++ [ ( "system", "Request failed" ) ] }, Cmd.none )
view m =
div []
[ div [] (List.map (\( r, c ) -> p [] [ b [] [ text (r ++ ": ") ], text c ]) m.log)
, input [ value m.input, onInput Typed ] []
, button [ onClick Send ] [ text "Send" ]
]
main =
Browser.element { init = \() -> ( Model "" [], Cmd.none ), update = update, view = view, subscriptions = \_ -> Sub.none }Ollama must allow your page's origin (set OLLAMA_ORIGINS) for browser requests to succeed.
ollama pull llama3.1, and keep it running. The code talks to its OpenAI-compatible API at http://localhost:11434/v1, so the same program also works with vLLM, llama.cpp server or a cloud API by changing the URL, model name and adding an API key.Elm learning roadmap
Tick items off as you go. Your progress is saved in this browser.
Beginner
Intermediate
Advanced
Projects to build
Elm cheat sheet
| Task | Syntax |
|---|---|
| Function | add a b = a + b |
| Annotation | add : Int -> Int -> Int |
| Lambda | \x -> x * 2 |
| Pipe | xs |> List.map f |
| Case | case m of Just v -> v; Nothing -> 0 |
| Record update | { model | count = 1 } |
Common mistakes
- There are no exceptions or null; handle every Maybe and Result.
- JavaScript interop only happens through ports or custom elements.
- Strings join with
++, not+.
Where to go next
- Elm guide — official
- Elm packages — libraries
- Elm Discourse — community
On TensorCore.si: JavaScript volume, Haskell volume.
Keyboard: [ previous language · ] next language · / search the library