View Types: A Hard Problem That a Region Makes Easy
A value whose fields point into its own other fields — a document holding slices into its own text — is one of the genuinely hard problems in language design; Rust needs Pin and unsafe, others reach for dependent types. Mere solves it with three small rules, and the reason it can is that the region built in the last two episodes was already doing the hard part.
Both previous episodes ended pointing at the same deferred question: what does it mean to hold a reference into a region, and how do you stop it from outliving what it points at? The reason it kept getting deferred is that, underneath, it is one of the genuinely hard problems in language design — and it is worth seeing why before seeing how small Mere’s answer turns out to be.
The self-referential value
Consider a document you parse once and then keep, holding on to slices of its own text:
struct Document { text: owned String }
// wanted: parse text once, keep the tokens as slices into it
view DocumentView of Document {
tokens: Slice<&self.text, str>, // points inside self.text
}
tokens points into Document.text. This is exactly the shape a parser wants —
tokenize the buffer, then carry around cheap slices instead of copying substrings
— and it is a minefield. The moment the Document moves, text sits at a new
address and every slice in tokens is dangling. The type system also has to be
able to say “a reference to another field of the same value,” and construction
has to build text first and the slices second, and teardown has to drop the
slices before the text or it passes through an invalid state.
This is not a corner case; it is a famous one. Rust pins the value in place with
Pin<T> and requires unsafe to actually form the self-reference. Languages
with dependent types can express it exactly, at a cost most programs shouldn’t
pay. The problem is real, and the usual solutions are heavy.
Three axioms
Mere’s answer is deliberately small — three rules, and no unsafe:
A — a view is an immutable bundle. A view fixes its internal reference relationships at construction time. After that it cannot be mutated and it cannot be moved; it is used only through immutable references. Nothing can change the address of a field, so no slice into it can go stale.
B — a view is built inside a region. A view lives on a region, not on some free-floating heap. Its lifetime is the region’s lifetime, and because every field points into that same region, all the field-to-field references are internal to one area with a single fate.
C — view V[R] of T is real syntax. A view is declared as a derived form of
a struct, parameterized by the region it lives in. The parameter R is how the
type system says, out loud, “all of this is inside region R” — the constraint is
in the type, not in your head.
Written out, a view and its constructor are ordinary code:
view DocumentView[region R] of Document {
own: &R Document, // the Document itself, placed in region R
tokens: &R [&R str], // slices pointing into own.text
}
fn make_view[region R](src: &borrowed str, r: &R Region) -> &R DocumentView[R] {
let doc = r.alloc(Document { text: copy_to_region(src, r) })
let tokens = tokenize(doc.text, r) // slices into doc.text, same region
r.alloc(DocumentView { own: doc, tokens: tokens })
}
No new construction syntax, no escape hatch — just allocation into a region and an ordinary record.
Why it’s safe, and why that’s not a coincidence
Walk the ways a self-reference normally goes wrong, and each one is closed by a rule you have already met:
- The value can’t move (axiom A), so the address a slice points at never changes.
- It can’t be mutated (axiom A), so the relationship set up at construction can’t be broken later.
- Every field points inside the same region (axiom B), and a region frees all at once (the unification from two episodes ago) — so nothing a slice points at is ever freed before the slice. There is no invalid intermediate state to pass through on teardown; the whole area goes at once.
- A view can’t be carried out of its region (the
Rin the type, axiom C), so there is no path by which it outlives what it points into.
The striking thing is how little of this is about views. Almost all of the
safety comes from properties the region already had: it doesn’t move, it frees in
bulk, references into it are typed with R. The view rules add only “immutable,
and declared with of T.” A problem that costs other languages Pin and
unsafe, or a dependent type system, costs Mere three small rules — because the
memory model built in the previous episodes was already carrying the weight. This
is what it looks like when collapsing two concepts into one pays off downstream:
the next hard feature turns out to be nearly free.
There is a philosophical point here that the language keeps returning to. Rust’s
honest answer to self-reference is unsafe — an explicit hole in the guarantees,
which is at least honest, but it is a hole. Mere would rather constrain the
feature until it needs no hole: a view can only be immutable, region-bound, and
non-movable, and in exchange it is checkable with no escape hatch at all. Less
expressive than “anything, anywhere, with unsafe,” and that is the trade being
made on purpose — the cases it does cover (parse trees, string interning,
documents and their indices) are covered with a guarantee you can read off the
types.
What a view costs at runtime
Nearly nothing, and for a satisfying reason. If every field of a view is either a
region-internal reference or a primitive, then the whole view is Trivial in
that region — the constraint from the last episode. So a view needs no cleanup of
its own: it is reclaimed when its region is, by the same single pointer reset that
frees everything else there. The type-level story and the runtime story line up —
a view is safe and free for the same reason, that it lives and dies with one
region.
Part II set out to give Mere an explicit, checkable memory model, and with view
types the shape of it is complete: five strategies narrowed to a unified region,
a Trivial line dividing what a region can hold from what with must clean up,
and now self-references made safe by staying inside a region. What’s left is to
make it usable — the everyday containers a program actually reaches for. Next:
region-aware collections, and the Trivial hierarchy that decides which of
them can nest inside which.