Values That Never Change
In Mar, every value is immutable. Not “immutable by convention”, not “please use const”: there is no assignment operator, no way to modify a record in place, no way to push onto a list. A value, once created, is what it is forever.
If you have spent years mutating variables, this sounds like being asked to cook without heat. This chapter explains what “change” means in an immutable world, why the restriction pays for itself, and why it does not have the performance cost you probably suspect.
The problem with things that change
Consider a line you have written a thousand times:
// JavaScript
user.name = "Ana";
Innocent. But answer honestly: who else has a reference to user? Whatever list it sits in, whatever cache holds it, whatever component rendered it, all of them just changed too, silently. Mutation is action at a distance: a write in one place is a surprise in every other place that shares the value.
Almost every “how did the state get like this?” bug is this pattern. Something, somewhere, changed a value that something else was relying on. The debugging session is an archaeology dig: not “what is wrong” but “who touched this, and when”.
Immutability deletes the question. If no value ever changes, then whatever a function received is exactly what it holds for as long as it wants. Nobody can pull the rug.
What “change” becomes
Programs still need to express change: a task gets renamed, a counter goes up. In Mar you do not modify the old value; you compute a new one that differs where you say:
rename : String -> Task -> Task
rename newName task =
{ task | name = newName }
{ task | name = newName } is record update syntax: “a new record, same as task, except name”. The original task is untouched; both values exist, and each reference keeps meaning what it meant.
Lists work the same way. There is no push; there are functions that produce new lists:
withDone : List Task -> List Task
withDone tasks =
List.filter (\t -> t.done) tasks
The habit shift is real but small: instead of “reach in and change it”, you write “here is the new whole”. After a week it stops feeling like a restriction; it starts feeling like the difference between editing a shared document and sending versioned copies. Nobody’s edits collide, because there are no edits.
Where state actually lives
An application obviously has state: the current user, the list on screen, the score. In Mar, state lives in one value, called the Model, and the runtime replaces it wholesale as your program computes new versions. Chapter “The Loop” makes this precise; the important idea here is the division of labor:
- Your code: pure computation from old value to new value.
- The runtime: the single mutable cell that swaps old for new.
All the mutation in a Mar program happens in one place you never touch, which means “who changed this?” always has the same answer: your update function, on the previous line of the message log.
The payoff, concretely
Bugs that stop existing. Shared-mutable-state bugs, iterator invalidation, “changed during render”, stale caches of live objects, defensive copying because you cannot trust a callee: all of these require mutation to happen. No mutation, no bug class.
Fearless sharing. Because nothing changes, sharing is always safe. Pass your whole Model to a function; it cannot damage it. Keep the old Model next to the new one; they cannot interfere.
Time travel, for free. Mar’s dev mode keeps every Model your app has ever had and lets you scrub back and forth through them like video. This is not a heroic engineering feat; it falls out of immutability. Old states are just values, and values keep.
Honest equality. When values cannot change, “equal now” means “equal forever”, so caching and change-detection become trustworthy.
“But copying everything must be slow”
The standard objection, and the answer is structural sharing. When you write { task | name = newName }, Mar does not deep-copy the record; the new value reuses everything from the old one except the field that differs. A new list that adds an element in front of an old list shares the old list entirely. Because nothing can be mutated, sharing is undetectable and therefore always safe. That is the trick: immutability is what makes the cheap implementation correct.
Is it literally as fast as mutating a field in C? No. Is it fast enough that entire 60-frames-per-second games, with hundreds of units doing pathfinding, ship in Mar today? Yes, and you will meet them in the Canvas chapter. For the CRUD apps, dashboards, and tools Mar targets, the difference is noise.
The mental model to keep
A Mar program never has “an object at time T”. It has values, and functions from values to values. Time is modeled the only honest way: as a sequence of distinct values, each one complete, none of them ever revised.
Everything else in this book leans on that sentence. The next chapter applies the same discipline to functions themselves.