Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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