Skip to content
Volume E · The TensorCore.si Library

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.

Prologue

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.
Prologue

Set up and run your first program

Install Elm from elm-lang.org or with npm. elm reactor serves files for quick experiments.

terminal
npm install -g elm
elm init
elm reactor   # open http://localhost:8000
src/Main.elm
module Main exposing (main)

import Html exposing (text)

name = "Ada"

main =
    text ("Hello, AI! Welcome to Elm, " ++ name ++ ".")
TipUse ++ to join strings. Indentation matters.
Chapter 1

Values and functions

Values never change. Functions are defined by name and parameters with no parentheses or commas, and type annotations document them.

Basics.elm
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.06
Chapter 2

Custom 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.

Device.elm
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 v
Chapter 3

Maybe, lists and records

There is no null: Maybe is Just value or Nothing. Records hold named fields, and the |> pipe chains list functions.

Gpus.elm
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.head
Chapter 4

The Elm Architecture

Every Elm app has a Model (state), an update function (how messages change state) and a view (HTML from state).

Main.elm
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 }
Chapter 5

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.

Stars.elm
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
        }
Part II · AI with Elm

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

elm/httpHTTP requests.
elm/jsonSafe JSON decoders.
PortsTalk to JavaScript libraries.
elm-uiLayout without CSS headaches.

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.

Llm.elm
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 [] ]))
Part III · Build an AI agent

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.

Your goal→Model plans→Tool call→Your code runs the tool→Result back to model→Final answer
Main.elm
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.

Run it locallyInstall Ollama, run 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.
Part IV · Your path

Elm learning roadmap

Tick items off as you go. Your progress is saved in this browser.

Beginner

Intermediate

Advanced

Part IV · Your path

Projects to build

Counter and to-doClassic Elm Architecture apps.
Weather appFetch and decode a JSON API.
LLM chat UIA typed chat interface for a local model.
Reference

Elm cheat sheet

TaskSyntax
Functionadd a b = a + b
Annotationadd : Int -> Int -> Int
Lambda\x -> x * 2
Pipexs |> List.map f
Casecase m of Just v -> v; Nothing -> 0
Record update{ model | count = 1 }
Reference

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 +.
Reference

Where to go next

On TensorCore.si: JavaScript volume, Haskell volume.

Keyboard: [ previous language · ] next language · / search the library