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.