Appendix A: Syntax Cheat Sheet
A one-stop reference for the syntax used throughout the book. Everything here is real Mar.
Values and functions
answer = 42 -- a definition (not a variable; it never changes)
double : Int -> Int -- type annotation (optional, recommended on top level)
double n = n * 2
add : Int -> Int -> Int -- two arguments
add a b = a + b
half = \n -> n // 2 -- lambda (anonymous function)
result = users |> List.filter isActive |> List.map score -- pipeline: x |> f == f x
Strings concatenate with ++. Comparison: ==, /=, <, >, <=, >=. Logic: &&, ||, and not (a function, not a prefix operator: not busy). Numbers: max, min, clamp low high x, abs, modBy d n (wraps, takes the divisor’s sign), remainderBy d n (in step with //). An integer literal adapts to Decimal from context: price : Decimal = 1 works and 1 + 1.50 is 2.50, but n + 1.5 with n : Int does not.
Numbers
Two number types, both exact: Int (whole, 53 bits — leaving the range raises) and Decimal (base-10, written 19.99). + - * work on either. Two quantities never mix: with n : Int, n + 1.5 is a type error, and Decimal.fromInt n is how you cross. Literals are the exception described above.
A JSON number arriving from outside is read as text: an Int if it is whole and inside 53 bits, an exact Decimal if it fits 34 significant digits, refused otherwise. 1e30 and its thirty-one digits are the same value and decode the same way.
n = 7 // 2 -- integer division, truncates (3); n // 0 == 0
r = modBy 3 7 -- 1; wraps, so modBy 3 -1 == 2
third = 1.0 / 3.0 |> Decimal.rounded Decimal.HalfEven 4 -- 0.3333
split = Decimal.withRemainder 2 (100.00 / 3)
-- { quotient = 33.33, remainder = 0.01 }
/ works only on Decimals and returns a Decimal.Division (the unresolved exact quotient); the two resolvers above are the only exits, so every rounding names its mode (Decimal.HalfEven, HalfUp, Down, Up, Floor, Ceiling) and scale at the call site. Handy conversions: Decimal.toString, Decimal.fromString, Decimal.toScale, Decimal.round / floor / ceiling / truncate (to Int), Decimal.fromCents / toCents.
Angles and trigonometry
An angle is an Angle, not a number, and the constructor names the unit. Any Int works, because construction wraps.
a = Math.degrees 45 -- also: Math.deciDegrees 450, Math.turns 32
b = Math.add a (Math.degrees 350) -- 35°, wrapped for you
c = Math.opposite a -- 225°
Math.sin (Math.degrees 30) -- 500 (thousandths, -1000..1000)
Math.cos (Math.degrees 60) -- 500
Math.atan2 1 1 -- Math.degrees 45 (y first, y points up)
Math.isqrt 17 -- 4 (whole part; 0 at or below zero)
Every one is total (Math.atan2 0 0 is 0°, Math.isqrt -5 is 0) and every one is identical on all three runtimes, because they read one generated table instead of the host’s trigonometry. Canvas.Rotate takes an Angle too, so a heading goes straight from Math to the canvas.
Records
type alias User =
{ id : Int
, name : String
}
u = { id = 1, name = "Ana" } -- build
u.name -- read
{ u | name = "Bia" } -- "update": a new record differing in one field
Unions and pattern matching
type Status
= Open
| Assigned User
| Closed
describe : Status -> String
describe status =
case status of
Open -> "open"
Assigned user -> "assigned to " ++ user.name
Closed -> "closed"
case must cover every constructor. _ is the wildcard pattern. Records destructure in patterns too.
Maybe and Result
case Repo.findById users id of
Just user -> greet user
Nothing -> showNotFound
case outcome of
Ok value -> use value
Err why -> explain why
Conditionals and locals
size = if n > 100 then "big" else "small" -- if is an expression; else is mandatory
area =
let
w = x2 - x1
h = y2 - y1
in
w * h
Lists
xs = [ 1, 2, 3 ]
ys = 0 :: xs -- prepend
names = List.map (\u -> u.name) users
adults = List.filter (\u -> u.age >= 18) users
oldest = List.foldl (\u acc -> max u.age acc) 0 users
total = List.sum (List.map .amount expenses) -- Int or Decimal, one name
Modules
module Frontend.Home exposing (page)
import UI exposing (list, section, title, button)
import Frontend.Routes
File path mirrors module name (Frontend/Home.mar is module Frontend.Home). Only what a module exposings is visible. No import cycles.
The MVU page skeleton
type alias Model = { count : Int }
type Msg
= Increment
init : (Model, Cmd Msg)
init = ( { count = 0 }, Cmd.none )
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
Increment -> ( { model | count = model.count + 1 }, Cmd.none )
view : Model -> View Msg
view model = ...
page : Page
page =
Page.create
{ path = "/", title = "Counter"
, init = init, update = update, view = view
, subscriptions = always Sub.none
}
-- Presented over the page it was reached from, instead of pushed:
-- page = Page.sheet (Page.create { ... })
Effects, both sides
-- Frontend: commands
Service.call Shared.addTask { name = draft } Added
Cmd.perform GotNow Time.now
Cmd.batch [ cmdA, cmdB ]
Nav.pushTo Frontend.Routes.home
Nav.dismiss -- close a presented route (Page.sheet), or step back one
-- Frontend: subscriptions
Time.every (Time.millis 16) Tick
Keyboard.watch KeysChanged -- mirror: { down : List Keyboard.Key }
Gamepad.watch PadChanged -- mirror: connected, both sticks (-100..100), held buttons
-- Backend: tasks
let
row <- Repo.findById tasks id -- bind: run, name the result, continue
in
...
Task.succeed value
Task.fail "broken invariant"
Repo.all tasks |> Task.map List.length
Randomness
A Generator a is a recipe for a value, not the value itself. You run it two ways.
die : Random.Generator Int
die = Random.int 1 6 -- also: uniform, list, pair, map, map2, andThen, constant
-- Frontend: fire a Cmd, receive the value as a Msg (fresh OS entropy each call)
update msg model =
case msg of
Roll -> ( model, Random.generate Rolled die )
Rolled n -> ( { model | face = n }, Cmd.none )
The seeded form is pure and runs anywhere, backend included:
(face, next) = Random.step die (Random.initialSeed 42) -- same seed, same face, every runtime
Random.step takes a Random.Seed and returns (value, nextSeed) — thread the seed to keep going. Because stepping is pure, the Go server and the JS/iOS client replay the identical sequence from one seed, which is how a card game shuffles on the server yet every client can verify it. To start from real randomness on the backend, draw a seed from the operating system:
let
seed <- Random.seed -- Task Random.Seed: fresh OS entropy
in
Task.succeed (Random.step (Random.list 40 card) seed)
Only Random.seed and Random.generate touch real entropy, and both wear it in the type (Task / Cmd). Stepping a generator never does.
The fullstack triangle
-- Shared.mar: declare once
addTask : Service { name : String } AddTaskOutcome
addTask = Service.declare POST "/tasks"
-- Backend: implement
services = [ Auth.protect Shared.addTask addTaskImpl ]
-- Frontend: call
Service.call Shared.addTask { name = model.draft } Added