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

Functions You Can Trust

The second discipline: in Mar, a function’s output depends on its inputs, and calling it does nothing except produce that output. No hidden reads, no hidden writes, no surprises. This property is called purity, and Mar does not merely encourage it; the language is arranged so that impurity has nowhere to hide.

What purity means

A function is pure when both of these hold:

  1. Same inputs, same output. Always. Not “usually”. A pure function consults nothing but its arguments: no globals, no clock, no database, no random source.
  2. No side effects. Calling it changes nothing in the world: no network request, no file write, no log line, no mutation (the previous chapter already removed that one).
totalCents : List Item -> Int
totalCents items =
    List.sum (List.map (\i -> i.priceCents * i.qty) items)

You can know what this function does by reading it. Not guess: know. It cannot charge a card, cannot depend on Tuesday, cannot work in dev and fail in prod because of environment differences. Its type signature, List Item -> Int, is the whole story of its relationship with the universe.

That reliability compounds. A program assembled from pure functions can be understood one function at a time, tested with plain asserts (no mocks, nothing to stub), refactored by substitution (any expression can be replaced by its value, a property with the grand name referential transparency), and cached or reordered by the runtime without changing behavior.

But programs must touch the world

An app that cannot write to the database is a space heater. The functional trick, and the single most important idea in this part of the book, is:

An effect is described as a value, and the runtime performs it.

Your code never does the effect; it returns a description of the effect. Two types carry these descriptions, one per half of the app:

  • Cmd msg on the frontend: “runtime, please do this work, and deliver the outcome to me as a message.” Calling a service, navigating, asking for the time.
  • Task a on the backend: “a computation that, when run, produces an a.” Reading rows, writing rows, getting the current time.

Here is a frontend fragment. Note carefully what update does and does not do:

update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
    case msg of
        AddClicked ->
            ( { model | submitting = True }
            , Service.call Shared.addTask { name = model.draft } Added
            )

Service.call ... does not perform an HTTP request. It builds a value describing one: which service, which payload, and which message (Added) should carry the result back. update stays pure: given this message and this model, it always returns this pair. The runtime receives the pair, swaps in the new model, performs the described request, and later feeds Added (Ok ...) or Added (Err ...) back through update.

The same idea on the backend, where Task values chain:

toggle : Int -> Task (Maybe Task)
toggle id =
    let
        found <- Repo.findById tasks id
    in
    case found of
        Just task -> Repo.update tasks id { done = task.done == False }
        Nothing   -> Task.succeed Nothing

Repo.findById returns a Task, a description of a read. The let ... <- syntax chains: “when that value arrives, continue here.” The handler as a whole is a recipe the runtime executes; the logic inside stays a pure arrangement of that recipe.

Why bother with the ceremony?

Because “effects are values” buys three things that “just call fetch()” cannot:

The type system sees your effects. A function returning Task (List Task) visibly touches the world; a function returning List Task -> Int visibly does not. Impurity cannot hide in a helper three calls deep, because it would show in every signature on the way up. Mar even uses this for policy: a service declared with the GET verb is rejected at compile time if its handler reaches a write like Repo.update, because writes are visible in the types.

Effects become data you can manipulate. Values can be put in lists, batched, returned from case branches, built conditionally. “Do these three things” is Cmd.batch [a, b, c], an ordinary value built by ordinary code.

Testing and replay become trivial. Since update only describes effects, you can drive it with any sequence of messages and inspect the results, no network required. Mar’s time-travel debugger replays your session by refeeding messages, which only works because nothing in your code fires effects on its own.

The line in the sand

Everything inside your functions: pure. Everything at the edge, HTTP, database, clock, randomness: described by your functions, performed by the runtime. You will see this division called managed effects, and it is the load-bearing wall of the whole architecture. MVU on the frontend and services on the backend are both just shapes built against it.

One practical note before moving on: purity makes where state can change boring, and the next chapter makes how code flows equally boring. Boring, in both cases, is the point.