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

Everything Is an Expression

Languages like JavaScript and Python are built from two kinds of thing: expressions, which produce values (a + b, f(x)), and statements, which do things (if, for, return, assignment). Mar removes the second kind. Every construct in the language is an expression: it produces a value, and it composes with everything else.

This sounds like a syntax trivia point. It changes how programs are shaped.

if produces a value

In Mar, if is not a fork in control flow; it is a choice between two values:

label : Int -> String
label n =
    if n == 1 then "1 item" else String.fromInt n ++ " items"

Because if yields a value, both branches must exist and must have the same type. There is no “forgot the else” bug: a missing branch is a missing value, and the program does not compile. The ternary operator from other languages is not a special case here; it is just what if always is.

case is switch grown up

case inspects a value against patterns, and each branch produces the result:

statusLine : Status -> String
statusLine status =
    case status of
        Open       -> "Open"
        InProgress -> "In progress"
        Done       -> "Done"

Two properties make case the workhorse of Mar programs. First, patterns can destructure, binding the parts of a value as they match (Just user -> binds user). Second, the compiler checks exhaustiveness: cover every constructor or the program does not compile. Add a fourth Status next month and every case that needs updating becomes a compile error, a to-do list written by the compiler. The types chapter leans hard on this.

let names intermediate values

let ... in introduces local names for readability. It is still an expression: the whole block has the value of its body.

priceLabel : Cart -> String
priceLabel cart =
    let
        subtotal = totalCents cart.items
        shipping = shippingFor cart
    in
    formatMoney (subtotal + shipping)

Names in a let are definitions, not assignments; each is defined once, and there is no “reassign it below” (there is nothing to reassign; values do not change).

No early return, and why that is fine

There is no return in Mar. A function’s body is one expression, and the value of that expression is the answer. Coming from imperative guard clauses, this feels claustrophobic for about a day, and then you notice what you got:

Every path is visible in the shape of the code. An early return is control flow that exits sideways; readers must simulate the function line by line to know what runs. An expression has no sideways. if and case show every outcome as an indented branch, and the compiler has confirmed they are all there.

The guard-clause instinct maps onto case cleanly:

greeting : Maybe User -> String
greeting maybeUser =
    case maybeUser of
        Nothing   -> "Welcome, stranger"
        Just user -> "Welcome back, " ++ user.name

No loops, and what replaces them

There is no for and no while. Iteration is done by functions over collections:

names   = List.map (\u -> u.name) users
adults  = List.filter (\u -> u.age >= 18) users

List.map transforms every element; List.filter keeps some; List.foldl reduces a list to one value; and a dozen relatives cover the shapes between. What you lose is the freeform loop body with its accumulating mutations; what you gain is that each operation names its intent. filter cannot accidentally also transform. A map cannot break out halfway. Reading List.filter (\t -> t.done) tasks takes exactly as long as understanding it.

When no library function fits, recursion is the general tool: a function that calls itself on a smaller input. It appears rarely in application code, but it is there, and the games use it freely.

Pipelines: reading top to bottom

Nested calls read inside-out: f (g (h x)). The pipe operator |> feeds a value to a function, so the same computation reads in the order it happens:

activeNames : List User -> List String
activeNames users =
    users
        |> List.filter (\u -> u.active)
        |> List.map (\u -> u.name)
        |> List.sort

Pipelines are everywhere in idiomatic Mar. They are not magic; x |> f is exactly f x. They exist because code is read far more often than it is written.

What this adds up to

Statements let a function be a little machine with private choreography: set this, maybe return, loop, mutate, bail. Expressions force a function to be a derivation: the answer, in terms of the inputs, all branches accounted for. Combined with immutability and purity, this is why you can read a Mar function you have never seen and trust your reading. The compiler has already checked the parts human reviewers are worst at: the missing branch, the forgotten case, the path that returns nothing.

Next: the type system that makes those checks possible without you writing types everywhere.