Language Atlas (mini-courses for more languages) Module 12 – Elm and The Elm Architecture
Elm for JavaScript developers
Map JavaScript habits to Elm: records and curried functions, custom types and case, Maybe and Result instead of null and throw, and how Elm divides.
What you will learn
- Write records, curried functions and pipelines, and update a record without changing the original
- Model a value with several states as a custom type and handle every state with case
- Replace null and exceptions with Maybe and Result, and chain checks with Result.andThen
- Predict what Elm's //, modBy, remainderBy, round and truncate return, for negative and large values too
- Fix a compiler error from its message, and make a build script trust the exit status of elm make
Before you start
What you’ll learn · Before you start
What you will learn
- Write records, curried functions and pipelines, and update a record without changing the original
- Model a value with several states as a custom type and handle every state with case
- Replace null and exceptions with Maybe and Result, and chain checks with Result.andThen
- Predict what Elm's //, modBy, remainderBy, round and truncate return, for negative and large values too
- Fix a compiler error from its message, and make a build script trust the exit status of elm make
Before you start
On this page
In JavaScript, { loading: false, error: null, data: null } is a valid state that no screen knows how to draw, and
Number("4e1") turns a typed quantity into 40. In Elm, with the screen’s states as one type, the first cannot be
written, and the second gives Nothing: the language has no null, no exceptions for expected failures and no
automatic conversions.
Here you map what you write every day in JavaScript to Elm: records and functions that take their arguments one at a
time, custom types and case for values with several states, Maybe and Result for absence and failure, numbers
that divide and round in three ways you would not guess, and compiler errors that you fix from their messages. Every
Elm program here ran on Elm 0.19.3, through a worker under Node.js 24.21.0.
Values never change, and a function takes one argument at a time
A JavaScript object can be changed by any code that holds it. An Elm record cannot be changed at all: there is no
assignment. { chai | quantity = 3 } is a record update: Elm's { record | field = value }, which makes a new record with some fields changed and leaves the original as it was, like { ...record, field: value } in JavaScript. Elm has no assignment to a field. that makes a new record with
one field different, as { ...chai, quantity: 3 } does in JavaScript, and chai itself stays as it was. Functions
work differently too. withTax : Int -> Int -> Int reads “takes an Int, and returns a function that takes an
Int and returns an Int”: every Elm function is curried function: A function that takes its arguments one at a time: given the first, it returns a function that waits for the rest. Every Elm function works this way, so withTax 5 is itself a function that adds five percent., so withTax 5 is
already a function that adds five percent, ready for List.map:
port module Records exposing (main)
{-| Records that never change, a record update, a function that takes one
argument at a time, and a pipeline.
-}
port out : String -> Cmd msg
type alias Line =
{ item : String
, paise : Int
, quantity : Int
}
chai : Line
chai =
{ item = "chai", paise = 1200, quantity = 2 }
{-| Adds a tax of `percent` to an amount in paise, rounding a half paisa up.
-}
withTax : Int -> Int -> Int
withTax percent paise =
paise + (paise * percent + 50) // 100
lineTotal : Line -> Int
lineTotal line =
line.paise * line.quantity
row : String -> String -> String
row label value =
String.padRight 26 ' ' label ++ value
report : List String
report =
let
more =
{ chai | quantity = 3 }
withFivePercent =
withTax 5
in
[ row "chai.quantity" (String.fromInt chai.quantity)
, row "more.quantity" (String.fromInt more.quantity)
, row "chai.quantity again" (String.fromInt chai.quantity)
, row "withTax 5 1200" (String.fromInt (withTax 5 1200))
, row "withFivePercent 1450" (String.fromInt (withFivePercent 1450))
, row "List.map withFivePercent" (String.join ", " (List.map (withFivePercent >> String.fromInt) [ 1200, 1450, 999 ]))
, row "pipeline" ([ chai, more ] |> List.map lineTotal |> List.sum |> String.fromInt)
]
main : Program () () Never
main =
Platform.worker
{ init = \_ -> ( (), out (String.join "\n" report) )
, update = \_ model -> ( model, Cmd.none )
, subscriptions = \_ -> Sub.none
} The session builds each worker of basics/ with --optimize and runs it through run.cjs, which prints what the
worker sends out through its port, as in the previous lesson:
"""Builds and runs the three workers of basics/ that map JavaScript habits to Elm: records and functions, a custom type
with case, and Maybe and Result (session.py plays it)."""
from session import cache, run
cache()
run("""
cd basics
elm make src/Records.elm --optimize --output=build/records.js
node run.cjs Records
elm make src/Status.elm --optimize --output=build/status.js
node run.cjs Status
elm make src/Parse.elm --optimize --output=build/parse.js
node run.cjs Parse
""") Output
$ cd basics
$ elm make src/Records.elm --optimize --output=build/records.js
Dependencies ready!
Success! Compiled 1 module.
Records ───> build/records.js
$ node run.cjs Records
chai.quantity 2
more.quantity 3
chai.quantity again 2
withTax 5 1200 1260
withFivePercent 1450 1523
List.map withFivePercent 1260, 1523, 1049
pipeline 6000
$ elm make src/Status.elm --optimize --output=build/status.js
Success! Compiled 1 module.
Status ───> build/status.js
$ node run.cjs Status
Loading orders...
2 orders, 9150 paise
Could not load orders: timeout
$ elm make src/Parse.elm --optimize --output=build/parse.js
Success! Compiled 1 module.
Parse ───> build/parse.js
$ node run.cjs Parse
quantity "3" Ok 3
quantity " 4 " Ok 4
quantity "0" Err below 1: 0
quantity "12" Err above 10: 12
quantity "4e1" Err not a whole number: "4e1"
quantity "" Err not a whole number: ""
quantity "2.5" Err not a whole number: "2.5"
String.toInt "4e1" Nothing
List.head [] Nothing
List.head [ 5, 7 ] Just 5
withDefault 1 1
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 basics_session.py
Read the first block. more.quantity is 3, and chai.quantity is still 2 afterwards. withTax 5 1200 is 1260, and
withFivePercent 1450 is 1523, because five percent of 1450 paise is 72.5 and the function rounds a half up with
(paise * percent + 50) // 100. The last line comes from a pipeline. x |> f means f x, so a chain of pipes reads
in the order the data moves instead of from the inside out:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- Start with a value. The pipeline starts from the list [ chai, more ]: chai is 2 cups at 1200 paise, and more is the same record with quantity 3.
- The first pipe. x |> f means f x. The list becomes the last argument of List.map lineTotal, which multiplies paise by quantity in each record: [ 2400, 3600 ].
- Add them up. The next |> hands [ 2400, 3600 ] to List.sum, which returns 6000. Each step gets the value of the step before it as its last argument.
- Turn it into text. String.fromInt turns the Int 6000 into the String "6000". Elm never turns a number into text by itself, which the next section of errors shows.
- Read top to bottom. Written without pipes, the same value is String.fromInt (List.sum (List.map lineTotal [ chai, more ])), read from the inside out. The pipeline reads in the order the data moves.
Read a type annotation from the left
withTax : Int -> Int -> Int is Int -> (Int -> Int). Give a function fewer arguments than its type lists and
you get a function back, which is how List.map (withTax 5) works without a wrapper such as (p) => withTax(5, p).
Checkpoint
Check: Records, curried functions and pipes
A custom type lists every state, and case must handle each one
A screen that loads orders is loading, has loaded them, or has failed. In JavaScript that is often three fields,
loading, error and data, whose 8 combinations include states that mean nothing:
loading |
error |
data |
What it means |
|---|---|---|---|
true |
null |
null |
loading |
false |
null |
a list | loaded |
false |
a message | null |
failed |
| any of the other 5 | no screen knows how to draw it |
Elm writes the same thing as one custom type (Elm): An Elm type declared with type and a list of variants, such as Loading, Loaded (List Order) and Failed String. A value is exactly one variant, and a case over the type must handle every variant or the module does not compile. with three variants, and only those
three values exist. A case expression takes a Status apart, and each branch gets the data its variant carries:
port module Status exposing (main)
{-| One custom type for the three states of a list of orders, and a case
expression that takes it apart.
-}
port out : String -> Cmd msg
type alias Order =
{ id : String
, paise : Int
}
type Status
= Loading
| Loaded (List Order)
| Failed String
describe : Status -> String
describe status =
case status of
Loading ->
"Loading orders..."
Loaded orders ->
String.fromInt (List.length orders)
++ " orders, "
++ String.fromInt (List.sum (List.map .paise orders))
++ " paise"
Failed reason ->
"Could not load orders: " ++ reason
main : Program () () Never
main =
Platform.worker
{ init =
\_ ->
( ()
, out
(String.join "\n"
(List.map describe
[ Loading
, Loaded [ { id = "A-17", paise = 6750 }, { id = "A-18", paise = 2400 } ]
, Failed "timeout"
]
)
)
)
, update = \_ model -> ( model, Cmd.none )
, subscriptions = \_ -> Sub.none
} The session printed one line per variant: Loading orders..., 2 orders, 9150 paise and
Could not load orders: timeout. Now add a fourth state, Refreshing (List Order), to show old orders while new
ones load, as broken/src/Status.elm does without touching its case, and build it:
"""Builds broken/src/Status.elm, whose Status type has a fourth variant, Refreshing, that its case expression does not
handle yet (session.py plays it)."""
from session import cache, run
cache()
run("""
cd broken
! elm make src/Status.elm --output=/dev/null
""") Output
$ cd broken
$ elm make src/Status.elm --output=/dev/null
Dependencies ready!
Compiling ...-- MISSING PATTERNS --------------------------------------------- src/Status.elm
This `case` does not have branches for all possibilities:
23|> case status of
24|> Loading ->
25|> "Loading orders..."
26|>
27|> Loaded orders ->
28|> String.fromInt (List.length orders) ++ " orders"
29|>
30|> Failed reason ->
31|> "Could not load orders: " ++ reason
Missing possibilities include:
Refreshing _
I would have to crash if I saw one of those. Add branches for them!
Hint: If you want to write the code for each branch later, use `Debug.todo` as a
placeholder. Read <https://elm-lang.org/0.19.3/missing-patterns> for more
guidance on this workflow.
Detected problems in 1 module.
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 variant_session.py
The build stopped with MISSING PATTERNS, quoted the whole case with its line numbers and listed
Refreshing _ under Missing possibilities include. That is the
The compiler's check that the branches of a case expression cover every value of the type. In Elm a case that misses a variant does not compile, and the error lists the missing patterns.:
every case over Status without a branch for the new variant is a compile error, so a new state becomes a list of
places to update, not a blank screen in production. The hint suggests Debug.todo as a placeholder while you write
the branches, which an optimised build refuses, as the last section shows.
A catch-all branch that hides the next state
Ending a case with _ -> "Unknown" turns the missing-pattern error off: Refreshing would be shown as
“Unknown” with no warning. Name every variant, so the compiler can find each case that must learn about a new one.
Checkpoint
Check: Custom types and case
Maybe and Result put absence and failure into the type
Where JavaScript returns undefined or null, Elm returns a Maybe: Elm's type for a value that may be missing: Just a value, or Nothing. It takes the place of null and undefined, and code must say what happens in the Nothing case before it can use the value.: Just a value or
Nothing. List.head [] is Nothing and List.head [ 5, 7 ] is Just 5, and a Maybe Int cannot be used as an
Int until the code says what happens in the Nothing case, with case or with Maybe.withDefault. Where
JavaScript throws, Elm returns a Result (Elm): Elm's type for an operation that can fail: Ok with a value, or Err with a reason, as in Result String Int. It takes the place of exceptions for failures a program expects, such as text that is not a number.: Ok a value or Err a reason.
quantity in Parse.elm reads a quantity typed into a form. It trims the text, asks String.toInt for a whole
number, turns Nothing into an Err with a message, and then checks the range with Result.andThen:
port module Parse exposing (main)
{-| Maybe and Result instead of null and exceptions: reading a quantity typed
into a form.
-}
port out : String -> Cmd msg
{-| A quantity from 1 to 10, or the reason the text is not one.
-}
quantity : String -> Result String Int
quantity text =
String.toInt (String.trim text)
|> Result.fromMaybe ("not a whole number: \"" ++ text ++ "\"")
|> Result.andThen inRange
inRange : Int -> Result String Int
inRange n =
if n < 1 then
Err ("below 1: " ++ String.fromInt n)
else if n > 10 then
Err ("above 10: " ++ String.fromInt n)
else
Ok n
show : Result String Int -> String
show result =
case result of
Ok n ->
"Ok " ++ String.fromInt n
Err reason ->
"Err " ++ reason
showMaybe : Maybe Int -> String
showMaybe maybe =
case maybe of
Just n ->
"Just " ++ String.fromInt n
Nothing ->
"Nothing"
lines : List String
lines =
List.map (\text -> String.padRight 21 ' ' ("quantity \"" ++ text ++ "\"") ++ show (quantity text))
[ "3", " 4 ", "0", "12", "4e1", "", "2.5" ]
++ [ "String.toInt \"4e1\" " ++ showMaybe (String.toInt "4e1")
, "List.head [] " ++ showMaybe (List.head [])
, "List.head [ 5, 7 ] " ++ showMaybe (List.head [ 5, 7 ])
, "withDefault 1 " ++ String.fromInt (Maybe.withDefault 1 (String.toInt "x"))
]
main : Program () () Never
main =
Platform.worker
{ init = \_ -> ( (), out (String.join "\n" lines) )
, update = \_ model -> ( model, Cmd.none )
, subscriptions = \_ -> Sub.none
} The session’s third block shows each input. " 4 " gave Ok 4, because the function trims first. "4e1", which
JavaScript’s Number reads as 40, gave Err not a whole number: "4e1": String.toInt accepts digits with an
optional sign and nothing else. "0" and "12" are whole numbers, and inRange turned them into Err below 1: 0
and Err above 10: 12. Follow three of them through the chain:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Trim first. String.toInt refuses " 4 " because of the spaces, so quantity trims the text first and passes on "4".
- Maybe a number. String.toInt "4" is Just 4. For "4e1" it is Nothing: Elm reads digits with an optional sign, not the exponent that JavaScript's Number accepts.
- A reason instead of Nothing. Result.fromMaybe turns Just 4 into Ok 4 and Nothing into Err with the message. From here the value carries the reason it failed.
- Nothing skips the rest. An Err passes through andThen untouched, so "4e1" leaves the chain as Err not a whole number: "4e1", and inRange never runs for it. A form would show the reason and let the person type again.
- The range check. Result.andThen inRange runs only on an Ok. For "12" it finds 12 above 10 and returns Err above 10: 12.
- The happy path. For " 4 " every step succeeds, and quantity returns Ok 4. The caller must take the value out with case, as show does, so the failure cases cannot be forgotten.
Result.andThen is what keeps the chain flat: it runs the next step only on an Ok, and passes an Err through
untouched, so the first failure is the one reported.
Key idea
A missing value is a Maybe and an expected failure is a Result; the type tells every caller to handle both
cases, and Result.andThen chains the steps so that the first Err is the answer.
Checkpoint
Check: Maybe and Result
Numbers divide as in JavaScript, with three surprises
An Elm Int is a JavaScript number, and most arithmetic is JavaScript’s own, but three operations have rules of
their own. The Numbers worker prints one expression a line, and compare.mjs prints the matching JavaScript:
port module Numbers exposing (main)
{-| How Elm divides, rounds and stores numbers, one expression a line.
-}
port out : String -> Cmd msg
row : String -> String -> String
row expression value =
String.padRight 22 ' ' expression ++ value
lines : List String
lines =
[ row "7 // 2" (String.fromInt (7 // 2))
, row "-7 // 2" (String.fromInt (-7 // 2))
, row "modBy 2 -7" (String.fromInt (modBy 2 -7))
, row "remainderBy 2 -7" (String.fromInt (remainderBy 2 -7))
, row "modBy -3 7" (String.fromInt (modBy -3 7))
, row "7 // 0" (String.fromInt (7 // 0))
, row "remainderBy 0 7" (String.fromInt (remainderBy 0 7))
, row "round 2.5" (String.fromInt (round 2.5))
, row "round -2.5" (String.fromInt (round -2.5))
, row "truncate -2.7" (String.fromInt (truncate -2.7))
, row "floor -2.7" (String.fromInt (floor -2.7))
, row "2147483647 // 1" (String.fromInt (2147483647 // 1))
, row "2147483648 // 1" (String.fromInt (2147483648 // 1))
, row "3000000000 // 100" (String.fromInt (3000000000 // 100))
, row "truncate 3.0e9" (String.fromInt (truncate 3.0e9))
, row "floor 3.0e9" (String.fromInt (floor 3.0e9))
, row "2 ^ 53 + 1" (String.fromInt (2 ^ 53 + 1))
, row "0.1 + 0.2" (String.fromFloat (0.1 + 0.2))
, row "toFloat 7 / 2" (String.fromFloat (toFloat 7 / 2))
]
main : Program () () Never
main =
Platform.worker
{ init = \_ -> ( (), out (String.join "\n" lines) )
, update = \_ model -> ( model, Cmd.none )
, subscriptions = \_ -> Sub.none
} """Builds and runs the number workers of basics/: the table of Numbers, the sign of a refund in Refund, and modBy 0 in
ModZero, once in development mode and once optimised (session.py plays it)."""
from session import cache, run
cache()
run("""
cd basics
elm make src/Numbers.elm --optimize --output=build/numbers.js
node run.cjs Numbers
elm make src/Refund.elm --optimize --output=build/refund.js
node run.cjs Refund
elm make src/ModZero.elm --output=build/modzero.js
! node run.cjs ModZero
elm make src/ModZero.elm --optimize --output=build/modzero.js
! node run.cjs ModZero
""") Output
$ cd basics
$ elm make src/Numbers.elm --optimize --output=build/numbers.js
Dependencies ready!
Success! Compiled 1 module.
Numbers ───> build/numbers.js
$ node run.cjs Numbers
7 // 2 3
-7 // 2 -3
modBy 2 -7 1
remainderBy 2 -7 -1
modBy -3 7 -2
7 // 0 0
remainderBy 0 7 NaN
round 2.5 3
round -2.5 -2
truncate -2.7 -2
floor -2.7 -3
2147483647 // 1 2147483647
2147483648 // 1 -2147483648
3000000000 // 100 30000000
truncate 3.0e9 -1294967296
floor 3.0e9 3000000000
2 ^ 53 + 1 9007199254740992
0.1 + 0.2 0.30000000000000004
toFloat 7 / 2 3.5
$ elm make src/Refund.elm --optimize --output=build/refund.js
Success! Compiled 1 module.
Refund ───> build/refund.js
$ node run.cjs Refund
paise rupees signedRupees
150 ₹1.50 ₹1.50
-150 ₹-1.50 -₹1.50
-5 ₹0.95 -₹0.05
-199 ₹-1.01 -₹1.99
$ elm make src/ModZero.elm --output=build/modzero.js
Success! Compiled 1 module.
ModZero ───> build/modzero.js
$ node run.cjs ModZero
Compiled in DEV mode. Follow the advice at https://elm-lang.org/0.19.3/optimize for better performance and smaller assets.
ModZero stopped: Cannot perform mod 0. Division by zero error.
[exit status 1]
$ elm make src/ModZero.elm --optimize --output=build/modzero.js
Success!
ModZero ───> build/modzero.js
$ node run.cjs ModZero
ModZero stopped: https://github.com/elm/core/blob/1.0.0/hints/11.md
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 numbers_session.py
// The JavaScript expressions that match the lines of Numbers.elm, as Node.js prints them.
const rows = [
['Math.trunc(7 / 2)', Math.trunc(7 / 2)],
['Math.trunc(-7 / 2)', Math.trunc(-7 / 2)],
['-7 % 2', -7 % 2],
['7 % -3', 7 % -3],
['7 / 0', 7 / 0],
['7 % 0', 7 % 0],
['Math.round(2.5)', Math.round(2.5)],
['Math.round(-2.5)', Math.round(-2.5)],
['Math.trunc(-2.7)', Math.trunc(-2.7)],
['2147483648 | 0', 2147483648 | 0],
['Math.trunc(3e9)', Math.trunc(3e9)],
['2 ** 53 + 1', 2 ** 53 + 1],
['0.1 + 0.2', 0.1 + 0.2],
['Number("4e1")', Number('4e1')],
['parseInt("4e1", 10)', parseInt('4e1', 10)],
];
for (const [expression, value] of rows) console.log(expression.padEnd(22) + String(value)); Output
Math.trunc(7 / 2) 3
Math.trunc(-7 / 2) -3
-7 % 2 -1
7 % -3 1
7 / 0 Infinity
7 % 0 NaN
Math.round(2.5) 3
Math.round(-2.5) -2
Math.trunc(-2.7) -2
2147483648 | 0 -2147483648
Math.trunc(3e9) 3000000000
2 ** 53 + 1 9007199254740992
0.1 + 0.2 0.30000000000000004
Number("4e1") 40
parseInt("4e1", 10) 4
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node compare.mjs
| Expression | Elm 0.19.3 | JavaScript, Node.js 24.21.0 |
|---|---|---|
| −7 divided by 2 | -7 // 2 is −3 |
Math.trunc(-7 / 2) is −3 |
| remainder of −7 by 2 | modBy 2 -7 is 1, remainderBy 2 -7 is −1 |
-7 % 2 is −1 |
| 7 divided by 0 | 7 // 0 is 0, remainderBy 0 7 is NaN |
7 / 0 is Infinity, 7 % 0 is NaN |
| −2.5 rounded | round -2.5 is −2 |
Math.round(-2.5) is −2 |
| 2147483648 divided by 1 | 2147483648 // 1 is −2147483648 |
2147483648 | 0 is −2147483648 |
| 3.0e9 without its fraction | truncate 3.0e9 is −1294967296 |
Math.trunc(3e9) is 3000000000 |
| 2^53 + 1 | 9007199254740992 | 9007199254740992 |
The first surprise is the remainder. Elm has two: remainderBy keeps the sign of the number divided, like
JavaScript’s %, and modBy keeps the sign of the modulus, so modBy 2 -7 is 1. That is
floor division: Integer division that rounds the quotient down, towards minus infinity, so -7 divided by 2 gives -4. Tcl's / on integers and Python's // divide this way, and their remainder takes the divisor's sign. More in “A one-week plan for learning any language” arithmetic, while // drops the fraction towards zero, which is
truncating division: Integer division that drops the fraction, rounding towards zero, so -7 divided by 2 gives -3. C, Objective-C and Visual Basic's \ divide this way, and their remainder takes the dividend's sign. More in “A one-week plan for learning any language”. The second is size. elm/core’s source computes // as
(a / b) | 0 and truncate as n | 0, and JavaScript’s | works on 32-bit integers, so a result past
2,147,483,647 wraps round: truncate 3.0e9 is −1294967296, while floor 3.0e9 gives 3000000000. The third is zero:
7 // 0 is 0 and remainderBy 0 7 is an Int that prints NaN, but modBy 0 7 stops the program, with
Cannot perform mod 0. Division by zero error. in development and only a link to a hint in the optimised build.
Rounding holds no surprise for a JavaScript developer: round is Math.round, so a half always goes up, towards
positive infinity, and round 2.5 is 3 while round -2.5 is −2.
The session also ran two smaller workers. ModZero.elm computes modBy 0 7, which no type rules out, and stopped
both times, as described above. Refund.elm prints amounts of paise with the rupees function of the Orders worker,
and beside it a version that handles the sign first:
port module Refund exposing (main)
{-| The rupees function of the Orders worker, given negative amounts, and a
version that handles the sign first.
-}
port out : String -> Cmd msg
rupees : Int -> String
rupees paise =
"₹" ++ String.fromInt (paise // 100) ++ "." ++ String.padLeft 2 '0' (String.fromInt (modBy 100 paise))
signedRupees : Int -> String
signedRupees paise =
if paise < 0 then
"-" ++ rupees (abs paise)
else
rupees paise
lines : List String
lines =
List.map
(\paise -> String.padLeft 6 ' ' (String.fromInt paise) ++ " " ++ String.padRight 10 ' ' (rupees paise) ++ signedRupees paise)
[ 150, -150, -5, -199 ]
main : Program () () Never
main =
Platform.worker
{ init = \_ -> ( (), out ("paise rupees signedRupees\n" ++ String.join "\n" lines) )
, update = \_ model -> ( model, Cmd.none )
, subscriptions = \_ -> Sub.none
} Mixing // and modBy for money
Orders.elm printed rupees as paise // 100 and modBy 100 paise. For a refund of −5 paise that is 0 rupees and
95 paise, so the Refund worker printed ₹0.95, and −199 paise came out as ₹-1.01. Take the sign first and
format abs paise, as signedRupees does: -₹0.05 and -₹1.99.
Checkpoint
Check: How Elm divides and rounds
Compiler errors explain themselves, and the exit status tells the truth
The last session builds the other two modules of broken/. Receipt.elm appends an Int to a String, which
JavaScript would turn into text without a word:
module Receipt exposing (line)
{-| A line of a receipt, with a mistake JavaScript would accept.
-}
line : String -> Int -> String
line item paise =
item ++ ": " ++ paise ++ " paise" """Two builds of broken/ that stop: a type mismatch, and an optimised build with a Debug.log left in, which fails while
the file of an earlier development build stays in place (session.py plays it)."""
from session import cache, run
cache()
run("""
cd broken
! elm make src/Receipt.elm --output=/dev/null
elm make src/Logged.elm --output=build/logged.js
node run.cjs Logged
! elm make src/Logged.elm --optimize --output=build/logged.js
grep -c "Compiled in DEV mode" build/logged.js
""") Output
$ cd broken
$ elm make src/Receipt.elm --output=/dev/null
Dependencies ready!
Compiling ...-- TYPE MISMATCH ----------------------------------------------- src/Receipt.elm
The (++) operator can append List and String values, but not Int values like
this:
9| item ++ ": " ++ paise ++ " paise"
^^^^^
Try using String.fromInt to turn it into a string? Or put it in [] to make it a
list? Or switch to the (::) operator?
Detected problems in 1 module.
[exit status 1]
$ elm make src/Logged.elm --output=build/logged.js
Success! Compiled 1 module.
Logged ───> build/logged.js
$ node run.cjs Logged
Compiled in DEV mode. Follow the advice at https://elm-lang.org/0.19.3/optimize for better performance and smaller assets.
total: 2650
total paise: 2650
$ elm make src/Logged.elm --optimize --output=build/logged.js
Compiling ...-- DEBUG REMNANTS --------------------------------------------------------------
There are uses of the `Debug` module in the following modules:
Logged
But the --optimize flag only works if all `Debug` functions are removed!
Note: The issue is that --optimize strips out info needed by `Debug` functions.
Here are two examples:
(1) It shortens record field names. This makes the generated JavaScript
smaller, but `Debug.toString` cannot know the real field names anymore.
(2) Values like `type Height = Height Float` are unboxed. This reduces
allocation, but it also means that `Debug.toString` cannot tell if it is
looking at a `Height` or `Float` value.
There are a few other cases like that, and it will be much worse once we start
inlining code. That optimization could move `Debug.log` and `Debug.todo` calls,
resulting in unpredictable behavior. I hope that clarifies why this restriction
exists!
Success!
[exit status 1]
$ grep -c "Compiled in DEV mode" build/logged.js
1
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 errors_session.py
The first error names the operator and the line, points at paise with carets and suggests the fix first: Try using String.fromInt to turn it into a string?. The second is subtler. Logged.elm keeps a
Debug.log from a debugging session; the development build printed total: 2650 and worked. The optimised build
printed DEBUG REMNANTS, because --optimize renames record fields and unboxes values that Debug would need, and
its last line on the screen was Success!, yet its exit status was 1. And build/logged.js was still there: grep
found the development build’s Compiled in DEV mode inside it. A deploy script that reads the screen, or that does
not stop on a failed step, uploads that old file. Step through what a script must check:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- The build runs. elm make --optimize finds Debug.log in Logged and prints DEBUG REMNANTS with the reason: --optimize strips what Debug needs.
- What the screen says. The progress line still ends with Success!, the word a person or a script looking at the text would see last.
- What the status says. The exit status is 1, and that is the answer to trust: the optimised build did not happen.
- The file that is left. build/logged.js is the file the earlier development build wrote, with its DEV mode warning inside, as grep -c found. A script that uploads it ships the wrong build.
- Stop on failure. So the script stops when the status is not 0, for example with set -e, or with elm make … && the next step; then the Debug.log is removed and the build runs again.
- Ship only a good build. Only after exit status 0 does the script minify and deploy, and it builds into a clean folder, so an old file can never pass for a new one.
Let the exit status stop the script
Start a deploy script with set -e, or join its steps with &&, so that elm make --optimize returning 1 ends
it; and build into a fresh folder, so a file from an earlier build can never be shipped as the new one.
Checkpoint
Check: Compiler errors and the exit status
Practise: port two money functions
Both steps are JavaScript, so their sample tests run right here. Step one turns Elm’s integer operators into
JavaScript functions that you can hold a port against, line by line: the tests are the rows of the table, the 32-bit
wrap and modBy 0 included. The second fixes the refund bug of Orders.elm in a function that must work for any whole
number of paise, and its tests are the four amounts of the Refund worker and a few more. Start with the step that
surprised you more:
Code lab · 2 steps
Port Elm's arithmetic, then fix refunds
Write //, modBy, remainderBy and round as Elm computes them, then print negative amounts of paise with their sign.
Exercise · Medium · JavaScript
Divide and round the way Elm does
A port from JavaScript to Elm is safe only if you know what each operator gives. Write four functions in intops.mjs that return exactly what Elm 0.19.3 returned in the lesson's Numbers table, so that you could check a port line by line:
idiv(a, b)for Elm'sa // b: the quotient without its fraction, which wraps like Elm's when it does not fit in 32 bits (2147483648 // 1is-2147483648), and is 0 whenbis 0.modBy(m, x)for Elm'smodBy m x: the remainder with the sign ofm(modBy 2 -7is 1,modBy -3 7is -2). Withmequal to 0 it throws anError, as Elm's program stopped.remainderBy(m, x)for Elm'sremainderBy m x: the remainder with the sign ofx(remainderBy 2 -7is -1).round(x)for Elm'sround x: halves go up, towards positive infinity (round -2.5is -2).
The order of the arguments follows Elm: the modulus comes first. The starter code is what a JavaScript developer writes from habit; the sample tests show where each function differs.
Starter code · intops.mjs
/** Elm's a // b. */
export function idiv(a, b) {
// Replace this: Math.floor rounds -3.5 down to -4, and Elm does not.
return Math.floor(a / b);
}
/** Elm's modBy m x: the remainder with the sign of the modulus. */
export function modBy(m, x) {
// Replace this: % keeps the sign of x.
return x % m;
}
/** Elm's remainderBy m x: the remainder with the sign of x. */
export function remainderBy(m, x) {
return x % m;
}
/** Elm's round x. */
export function round(x) {
// Replace this: Math.trunc drops the fraction instead of rounding.
return Math.trunc(x);
} The sample tests · intops.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { idiv, modBy, remainderBy, round } from './intops.mjs';
test('// drops the fraction, towards zero', () => {
assert.equal(idiv(7, 2), 3);
assert.equal(idiv(-7, 2), -3);
assert.equal(idiv(7, -2), -3);
assert.equal(idiv(-7, -2), 3);
});
test('// gives 0 for a divisor of 0, and wraps past 32 bits', () => {
assert.equal(idiv(7, 0), 0);
assert.equal(idiv(2147483647, 1), 2147483647);
assert.equal(idiv(2147483648, 1), -2147483648);
assert.equal(idiv(3000000000, 100), 30000000);
});
test('modBy takes the sign of the modulus', () => {
assert.equal(modBy(2, -7), 1);
assert.equal(modBy(3, 7), 1);
assert.equal(modBy(-3, 7), -2);
assert.equal(modBy(4, -5), 3);
assert.equal(modBy(100, -5), 95);
assert.equal(modBy(5, 10), 0);
assert.ok(modBy(5, -10) === 0, 'modBy 5 -10 is 0 (JavaScript may give -0, which is equal to 0)');
});
test('modBy 0 stops with an error', () => {
assert.throws(() => modBy(0, 7), Error);
});
test('remainderBy takes the sign of the number divided', () => {
assert.equal(remainderBy(2, -7), -1);
assert.equal(remainderBy(4, -5), -1);
assert.equal(remainderBy(-3, 7), 1);
});
test('round sends halves towards positive infinity', () => {
assert.equal(round(2.5), 3);
assert.equal(round(-2.5), -2);
assert.equal(round(-1.5), -1);
assert.equal(round(1.2), 1);
assert.equal(round(-1.8), -2);
}); A hint
Elm's // divides and then drops the fraction through a 32-bit integer, which in JavaScript is (a / b) | 0. remainderBy is JavaScript's own %. For modBy, start from x % m and add m when the result is not 0 and its sign differs from the sign of m.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Exercise · Easy · JavaScript
Print refunds without losing the sign
The starter code is the rupees function of the Orders worker, ported to JavaScript as it behaves in Elm: the rupees with Elm's //, which drops the fraction towards zero, and the paise with Elm's modBy 100, which is never negative. For 150 paise it prints "₹1.50", but for a refund of -5 paise it prints "₹0.95", as the Refund worker did.
Change signedRupees(paise) in signed.mjs so that it works for any whole number of paise: a minus sign before the rupee sign when the amount is below zero, then the amount without its sign, with two digits of paise. -5 paise is "-₹0.05", -199 paise is "-₹1.99", and 0 is "₹0.00", without a sign.
Starter code · signed.mjs
/** Elm's modBy 100: never negative. */
const modBy100 = (x) => ((x % 100) + 100) % 100;
/** Any whole number of paise as rupees, with a minus sign for a refund: -5 is "-₹0.05". */
export function signedRupees(paise) {
// Replace this: it is Orders.elm's rupees, which mixes a quotient towards zero with a remainder that is never negative.
return '₹' + Math.trunc(paise / 100) + '.' + String(modBy100(paise)).padStart(2, '0');
} The sample tests · signed.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { signedRupees } from './signed.mjs';
test('amounts of 0 or more are unchanged', () => {
assert.equal(signedRupees(150), '₹1.50');
assert.equal(signedRupees(6750), '₹67.50');
assert.equal(signedRupees(5), '₹0.05');
assert.equal(signedRupees(0), '₹0.00');
});
test('the four amounts of the Refund worker', () => {
assert.equal(signedRupees(150), '₹1.50');
assert.equal(signedRupees(-150), '-₹1.50');
assert.equal(signedRupees(-5), '-₹0.05');
assert.equal(signedRupees(-199), '-₹1.99');
});
test('whole rupees and large refunds', () => {
assert.equal(signedRupees(-100), '-₹1.00');
assert.equal(signedRupees(-123456), '-₹1234.56');
assert.equal(signedRupees(-1), '-₹0.01');
}); A hint
Take the sign out first and format the absolute value, as signedRupees in Refund.elm does with abs paise. Once the number is 0 or more, dropping the fraction and taking the remainder agree, so the split into rupees and paise is right.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Then the self-check, from records to compiler errors. It starts with the idea you know least, and every answer you pick is explained at once:
Practice that adapts to you
Practise this lesson
12 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher to mid-level): What do Maybe and Result replace in JavaScript, and how do you get a value out of one?
Maybe replaces null and undefined: a value is Just x or Nothing, and functions that may find nothing say so
in their type, as List.head and String.toInt do. Result replaces exceptions for failures the program expects:
Ok x or Err reason, with the reason’s type in the signature, such as Result String Int. The type forces the
caller to handle the other case, so there is no TypeError from a missing value. To get the value out, use case
with a branch for each variant, Maybe.withDefault when a default makes sense, or keep working inside with
Maybe.map and Result.andThen, which skip the work on Nothing or Err. In the lesson, quantity "4e1" returned
Err not a whole number: "4e1" where JavaScript’s Number would have read 40.
Two longer questions, one about a money bug and one about a deploy that shipped the wrong build:
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to senior
The refunds screen of your Elm app shows a refund of 5 paise as 0.95 rupees, while positive amounts look right and every existing test passes. The formatter uses paise // 100 and modBy 100 paise. Explain the bug, fix it, and say which tests would have caught it.
-
Mid to staff
After a release, the browser console of your Elm app shows "Compiled in DEV mode", and a Debug.log line prints customer totals. The CI log of the deploy ends with Success!. What happened in the pipeline, and how do you fix it so that it cannot happen again?
Flashcards · 8 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
What does { chai | quantity = 3 } do in Elm?
It builds a new record with quantity 3 and the other fields copied, like { ...chai, quantity: 3 }. chai stays as it was, since Elm has no assignment.
-
withTax : Int -> Int -> Int. What is withTax 5?
A function that adds five percent to a price: every Elm function is curried, so fewer arguments give a function back, ready for List.map (withTax 5).
-
Why model a loading screen as one custom type rather than loading, error and data fields?
Three fields allow 8 combinations, 5 of which no screen can draw. Loading, Loaded (List Order) and Failed String allow exactly the three real states.
-
A variant is added to a type and a case over it is not updated. What does the compiler do?
It stops with MISSING PATTERNS, quoting the case and listing the missing variant, so every case that must learn about it is found before the program runs.
-
What does String.toInt "4e1" return in Elm?
Nothing: it accepts digits with an optional sign and nothing else, while JavaScript's Number("4e1") gives 40.
-
What do modBy 2 -7 and remainderBy 2 -7 return, and what does modBy 0 do?
1 and −1: modBy takes the sign of the modulus, remainderBy that of the number divided, like JavaScript's %. modBy 0 stops the program, while 7 // 0 is simply 0.
-
Why is truncate 3.0e9 −1294967296 in Elm?
elm/core computes truncate as n | 0 and // as (a / b) | 0, and JavaScript's | works on 32 bits, so results past 2,147,483,647 wrap. floor 3.0e9 gives 3000000000.
-
How can a deploy ship an old build after elm make --optimize refuses a Debug.log?
The screen may still end with Success! while the exit status is 1, and the old file stays. Stop the script on the status, with set -e or &&, and build into a fresh folder.
Key takeaways
- Elm values never change: a record update makes a new record, every function is curried, and
|>passes a value on as the last argument of the next function. - A custom type lists every state a value can be in, and adding a variant turns each
casethat misses it into a compile error, asRefreshingdid. Maybestands for absence andResultfor expected failure;String.toInt "4e1"isNothing, andResult.andThenstops a chain at its firstErr.//drops the fraction towards zero,modBytakes the sign of the modulus andremainderBythat of the number,//andtruncatewrap past 2,147,483,647, andmodBy 0stops the program.- Compiler errors name the line and suggest the fix, and
elm make --optimizerefusesDebug; a script must stop on its exit status, because the screen may still end withSuccess!while an old file stays in place.
Terms in this lesson
- curried function
- A function that takes its arguments one at a time: given the first, it returns a function that waits for the rest. Every Elm function works this way, so withTax 5 is itself a function that adds five percent.
- custom type (Elm)
- An Elm type declared with type and a list of variants, such as Loading, Loaded (List Order) and Failed String. A value is exactly one variant, and a case over the type must handle every variant or the module does not compile.
- Maybe
- Elm's type for a value that may be missing: Just a value, or Nothing. It takes the place of null and undefined, and code must say what happens in the Nothing case before it can use the value.
- record update
- Elm's { record | field = value }, which makes a new record with some fields changed and leaves the original as it was, like { ...record, field: value } in JavaScript. Elm has no assignment to a field.
- Result (Elm)
- Elm's type for an operation that can fail: Ok with a value, or Err with a reason, as in Result String Int. It takes the place of exceptions for failures a program expects, such as text that is not a number.
References
- elm/core: Basics (arithmetic, round, truncate, modBy, remainderBy) (Elm)
- elm/core 1.0.5 source: Elm/Kernel/Basics.js (Elm)
- elm/core: Maybe (Elm)
- elm/core: Result (Elm)
- elm/core: String (toInt, fromInt) (Elm)
- An Introduction to Elm: Custom Types (Elm)
- An Introduction to Elm: Records (Elm)
- Elm compiler hint: missing patterns (Elm)
- Elm compiler hint: how to optimize Elm code (Elm)
- Math.round() (MDN Web Docs)
- Remainder (%) (MDN Web Docs)
- Bitwise OR (|) (MDN Web Docs)
- Number.MAX_SAFE_INTEGER (MDN Web Docs)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress