Vol. 5 · № 03 · Un atelier RustA Rust workshop

Orphan rule & blanket impls Orphan rule & blanket impls

Pourquoi on ne peut pas tout implémenter partout : la cohérence. Et les patterns autour — le newtype pour contourner l'orphan rule proprement. Why you can't implement everything everywhere: coherence. And the patterns around it — the newtype to work around the orphan rule cleanly.

AudienceAudience
Dev maîtrisant traits, generics et dyn (Vol 5 №1–2) Dev fluent in traits, generics and dyn (Vol 5 №1–2)
Format
Self-paced
ChapitresChapters
5
Date
Juin 2026 Jun 2026
≈ 18 min ●●●● TraitsCoherenceNewtype
SommaireContents ·
№ 01CadrageFraming3 min

Une implémentation, et une seule, pour tout le programme. C'est la cohérence.One implementation, and only one, for the whole program. That's coherence.

Deux numéros durant, on a implémenté des traits comme si de rien n'était. Mais une contrainte rôdait, évoquée au n°1 : on ne peut pas implémenter n'importe quel trait pour n'importe quel type. La raison est une garantie que Rust tient à tout prix — la cohérence : pour un trait et un type donnés, il existe au plus une implémentation dans tout le programme. Sans elle, deux bibliothèques pourraient donner au même type deux comportements incompatibles pour le même trait, et le compilateur, face à un appel, ne saurait laquelle choisir. L'orphan rule est le gardien de cette unicité : tu ne peux implémenter un trait pour un type que si tu possèdes au moins l'un des deux — le trait, ou le type.For two issues, we implemented traits as if nothing could go wrong. But a constraint lurked, hinted at in n°1: you can't implement just any trait for just any type. The reason is a guarantee Rust upholds at all costs — coherence: for a given trait and type, there is at most one implementation in the whole program. Without it, two libraries could give the same type two incompatible behaviors for the same trait, and the compiler, facing a call, wouldn't know which to choose. The orphan rule is the guardian of that uniqueness: you may implement a trait for a type only if you own at least one of the two — the trait, or the type.

L'orphan rule — au moins l'un des deux doit t'appartenirThe orphan rule — at least one of the two must be yours
// La règle : pour impl Trait for Type, tu dois POSSÉDER l'un des deux.

impl Surface for Cercle { /* ... */ }   // ✓ Surface est à toi (ou Cercle l'est)
impl Display for Cercle  { /* ... */ }   // ✓ Cercle est à toi (Display est std)

impl Display for Vec<i32> { /* ... */ }  // ✕ NI Display NI Vec ne sont à toi
//   error[E0117]: only traits defined in the current crate can be
//   implemented for types defined outside of the crate (orphan rule)

// les deux « étrangers » à la fois = interdit. sinon deux crates pourraient
// donner à Vec deux impl Display différentes — et laquelle gagnerait ?
Pourquoi « orphan » ?Why 'orphan'?
crate jolie-couleur
impl Display for Vec<u8>
crate console-log
impl Display for Vec<u8>
↓
↓
ton programme dépend des deuxyour program depends on both
↓
println!("{}", mon_vec)
??? quelle des deux impl ?which of the two impls?
→ conflit insoluble : l'orphan rule l'interdit en amont→ unsolvable conflict: the orphan rule forbids it upstream

Une implémentation est « orpheline » quand ni le trait ni le type ne sont définis dans ta crate — elle n'a de parent nulle part. Le danger est concret : imagine que la crate jolie-couleur et la crate console-log implémentent toutes deux Display pour Vec<u8>. Un programme qui dépend des deux contiendrait deux impl rivales du même couple (trait, type). Que devrait afficher println!("{}", mon_vec) ? Il n'y a pas de bonne réponse — et Rust refuse de choisir au hasard. L'orphan rule rend la situation impossible à créer : au plus une des deux crates peut écrire cette impl, et seulement si elle possède le type.An implementation is 'orphan' when neither the trait nor the type is defined in your crate — it has a parent nowhere. The danger is concrete: imagine the pretty-color crate and the console-log crate both implement Display for Vec<u8>. A program depending on both would contain two rival impls of the same (trait, type) pair. What should println!("{}", my_vec) print? There's no right answer — and Rust refuses to choose at random. The orphan rule makes the situation impossible to create: at most one of the two crates can write that impl, and only if it owns the type.

Le réflexe du numéroThe issue's reflex

« Le trait que je veux implémenter, ou le type sur lequel je l'implémente : au moins l'un des deux est-il à moi ? » Oui → l'impl est permise. Non (les deux viennent de crates externes) → l'orphan rule la refuse, et il faut un détour : envelopper le type étranger dans un type à toi. Ce détour, c'est le newtype du Vol 3, qui revient ici jouer un rôle inattendu.'The trait I want to implement, or the type I implement it on: is at least one of them mine?' Yes → the impl is allowed. No (both come from external crates) → the orphan rule refuses it, and you need a detour: wrap the foreign type in a type of your own. That detour is Vol 3's newtype, returning here to play an unexpected role.

Une limite, pas une punition.A limit, not a punishment.

L'orphan rule frustre la première fois qu'on la rencontre — on voulait juste ajouter un petit Display sur un Vec. Mais c'est le prix d'une propriété précieuse : en Rust, ajouter une dépendance ne peut jamais changer en silence le comportement d'un type que tu utilises déjà. Les impl sont globales et uniques, donc prévisibles. La cohérence est ce qui rend l'écosystème composable sans conflits invisibles.The orphan rule frustrates the first time you meet it — you just wanted to add a small Display on a Vec. But it's the price of a precious property: in Rust, adding a dependency can never silently change the behavior of a type you already use. Impls are global and unique, hence predictable. Coherence is what makes the ecosystem composable without invisible conflicts.

№ 02La surpriseThe surprise4 min

Le newtype du Vol 3 revient — cette fois pour débloquer une implémentation.Vol 3's newtype returns — this time to unlock an implementation.

Au Vol 3, le newtype servait à donner un sens et une garantie à un type primitif — Email(String). Le voici dans un rôle tout autre, et c'est la surprise du numéro : il est aussi la sortie standard de l'orphan rule. Si tu ne peux pas implémenter un trait étranger pour un type étranger, alors rends l'un des deux non étranger. Tu ne possèdes pas Vec, et tu ne possèdes pas Display ? Crée un type qui, lui, t'appartient — une simple enveloppe autour du Vec — et implémente le trait sur lui. La cohérence est respectée, parce que ce nouveau type est à toi seul : personne d'autre ne peut entrer en conflit. Le même outil, deux usages sans rapport apparent, unis par une idée : posséder le type, c'est avoir le droit de le doter de comportements.In Vol 3, the newtype gave meaning and a guarantee to a primitive type — Email(String). Here it plays an entirely different role, and that's the issue's surprise: it's also the standard exit from the orphan rule. If you can't implement a foreign trait for a foreign type, then make one of the two non-foreign. You don't own Vec, and you don't own Display? Create a type that you do own — a simple wrapper around the Vec — and implement the trait on it. Coherence is respected, because that new type is yours alone: no one else can conflict. The same tool, two seemingly unrelated uses, united by one idea: owning the type is having the right to endow it with behaviors.

Le mur — un trait étranger sur un type étrangerThe wall — a foreign trait on a foreign type
// On veut afficher joliment un Vec<u8> comme du hexadécimal.
// Display et Vec sont tous deux dans std → on ne possède NI l'un NI l'autre.

impl std::fmt::Display for Vec<u8> {
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        for b in self { write!(f, "{b:02x}")?; }
        Ok(())
    }
}
// error[E0117]: only traits defined in the current crate can be implemented
// for types defined outside of the crate. L'impl est orpheline : refusée. Impasse ? Non.

L'intention est légitime — afficher un Vec<u8> en hexadécimal — mais l'impl est orpheline : ni Display ni Vec ne t'appartiennent. Rust refuse, non parce que ce serait dangereux ici, mais parce qu'autoriser ce cas ouvrirait la porte aux conflits entre crates. La règle est volontairement large et syntaxique — « possèdes-tu l'un des deux ? » — pour rester décidable sans analyser tout le graphe de dépendances. Le refus n'est pas une impasse : c'est une invitation à passer par un type qui soit le tien.The intent is legitimate — display a Vec<u8> in hexadecimal — but the impl is orphan: neither Display nor Vec is yours. Rust refuses, not because it would be dangerous here, but because allowing this case would open the door to cross-crate conflicts. The rule is deliberately broad and syntactic — 'do you own one of the two?' — to stay decidable without analyzing the whole dependency graph. The refusal isn't a dead end: it's an invitation to go through a type that is yours.

Tu ne possèdes ni le trait ni le type ? Crée un type qui soit le tien — et le droit d'implémenter revient avec la propriété.You own neither the trait nor the type? Create a type that is yours — and the right to implement returns with ownership.
№ 03La mécaniqueThe mechanics5 min

Le blanket impl : doter d'un comportement tout un univers de types, d'un coup.The blanket impl: endow a whole universe of types with a behavior, at once.

L'orphan rule dit où l'on peut implémenter. Son pendant lumineux dit jusqu'où : le blanket impl, une implémentation non pour un type, mais pour tous les types qui satisfont une borne. impl<T: Display> MonTrait for T donne MonTrait, gratuitement, à chaque type qui sait déjà s'afficher — présents et futurs, y compris ceux que tu n'as jamais vus. C'est le mécanisme par lequel la bibliothèque standard distribue des comportements à grande échelle : tout Display devient ToString, tout type se convertit en lui-même via From. Le générique du n°1 abstraitait sur les valeurs ; le blanket impl abstrait sur les implémentations elles-mêmes.The orphan rule says where you may implement. Its bright counterpart says how far: the blanket impl, an implementation not for a type, but for all types satisfying a bound. impl<T: Display> MyTrait for T gives MyTrait, for free, to every type that already knows how to display itself — present and future, including those you've never seen. It's the mechanism by which the standard library distributes behaviors at scale: every Display becomes ToString, every type converts to itself via From. Issue n°1's generic abstracted over values; the blanket impl abstracts over implementations themselves.

impl<T: Borne> Trait for T — une impl pour toute une familleimpl<T: Bound> Trait for T — one impl for a whole family
// Un blanket impl : implémenter un trait pour TOUS les types qui en respectent un autre
trait Resume {
    fn resume(&self) -> String;
}

impl<T: std::fmt::Display> Resume for T {     // « pour tout T qui est Display… »
    fn resume(&self) -> String {
        format!("valeur = {self}")            // …voici une impl de Resume, gratuite
    }
}

42.resume();        // ✓ i32 est Display → il a Resume sans une ligne de plus
"salut".resume();   // ✓ &str aussi. tout l'univers des Display gagne Resume d'un coup.
La std en est tisséestd is woven from it
// La bibliothèque standard en regorge :
impl<T: Display> ToString for T { /* ... */ }   // tout Display sait .to_string()
impl<T> From<T> for T { /* ... */ }              // tout type se convertit en lui-même
impl<T, U: From<T>> Into<U> for T { /* ... */ }  // Into est dérivé de From, partout

Ces trois lignes expliquent des commodités qu'on prenait pour acquises depuis le début de la série. Tu n'as jamais écrit d'impl ToString : elle te vient du blanket impl sur tout Display. Le .into() qui convertissait nos erreurs sous le ? (Vol 4) ? Un blanket impl qui dérive Into de chaque From. Implémenter From, c'est obtenir Into gratuitement, partout, sans y penser. Le blanket impl est la couture invisible de l'écosystème.These three lines explain conveniences we'd taken for granted since the series began. You never wrote a ToString impl: it comes to you from the blanket impl over every Display. The .into() that converted our errors under the ? (Vol 4)? A blanket impl deriving Into from every From. Implementing From means getting Into for free, everywhere, without a thought. The blanket impl is the ecosystem's invisible seam.

Le revers : il interdit le chevauchementThe flip side: it forbids overlap

La cohérence reprend la main. Un blanket impl couvrant « tout T » consomme tout l'espace : tu ne peux plus, ensuite, écrire une impl spécifique pour un type particulier sans chevaucher le blanket, et Rust refuse deux impl applicables au même type. C'est la rançon de la puissance : un blanket impl est un engagement total, qui ferme la porte aux exceptions. D'où la prudence des bibliothèques à en exposer — un blanket impl public est presque impossible à retirer sans casser tout le monde.Coherence takes over again. A blanket impl covering 'every T' consumes the whole space: you can no longer write a specific impl for a particular type without overlapping the blanket, and Rust refuses two impls applicable to the same type. That's the price of the power: a blanket impl is a total commitment that shuts the door on exceptions. Hence libraries' caution in exposing them — a public blanket impl is nearly impossible to remove without breaking everyone.

L'extension trait.The extension trait.

Combine les deux idées : définis un trait à toi, puis un blanket impl sur une borne large, et tu ajoutes des méthodes à des types que tu ne possèdes pas — sans newtype. C'est le pattern « extension trait » : itertools enrichit ainsi tous les Iterator, et tu peux faire de même. La méthode n'est disponible que si ton trait est importé (use), ce qui évite la pollution. Une façon propre de « décorer » l'univers sans violer la cohérence.Combine both ideas: define a trait of your own, then a blanket impl on a broad bound, and you add methods to types you don't own — without a newtype. That's the 'extension trait' pattern: itertools enriches all Iterators this way, and you can too. The method is available only if your trait is imported (use), which avoids pollution. A clean way to 'decorate' the universe without breaking coherence.

Le blanket impl donne un comportement à tout un univers de types — au prix de ne plus jamais pouvoir y faire d'exception.The blanket impl gives a behavior to a whole universe of types — at the price of never being able to make an exception.
№ 04Le piègeThe trap4 min

Le newtype qui, via Deref, rouvre tout ce qu'il devait fermer.The newtype that, via Deref, reopens everything it was meant to close.

Le newtype a un revers connu : il perd les méthodes du type qu'il emballe. Un Hex(Vec<u8>) n'a plus .len() ni .iter() — il faut les ré-exposer une à une. Devant cette corvée, la tentation est d'implémenter Deref vers le type interne : d'un coup, toutes les méthodes du Vec reviennent, comme par magie. C'est presque toujours une erreur. Deref sur un newtype défait précisément ce que le newtype apportait : la barrière. Toutes les méthodes du type emballé refont surface, y compris celles qui peuvent violer l'invariant que l'enveloppe protégeait. On a écrit un type pour contraindre, puis on a percé un trou par lequel toute contrainte s'échappe. La bonne voie est plus modeste : exposer à la main, délibérément, les seules opérations qui respectent la garantie.The newtype has a known flip side: it loses the methods of the type it wraps. A Hex(Vec<u8>) no longer has .len() or .iter() — you must re-expose them one by one. Faced with that chore, the temptation is to implement Deref to the inner type: suddenly all of Vec's methods return, as if by magic. It's almost always a mistake. Deref on a newtype undoes precisely what the newtype brought: the barrier. Every method of the wrapped type resurfaces, including those that can violate the invariant the wrapper protected. You wrote a type to constrain, then drilled a hole through which all constraint escapes. The right path is humbler: expose by hand, deliberately, only the operations that respect the guarantee.

Le contre-exemple — Deref qui laisse fuir l'invariantThe counter-example — Deref leaking the invariant
// Le réflexe « newtype + Deref » pour récupérer TOUTES les méthodes du type emballé
struct IdsTries(Vec<u32>);   // invariant voulu : toujours trié

impl std::ops::Deref for IdsTries {
    type Target = Vec<u32>;
    fn deref(&self) -> &Vec<u32> { &self.0 }   // « rends-moi tout Vec, transparent »
}
impl std::ops::DerefMut for IdsTries {         // …et son pendant mutable, ajouté
    fn deref_mut(&mut self) -> &mut Vec<u32> { &mut self.0 }   // « pour que ça marche »
}

let mut ids = IdsTries(vec![1, 2, 5]);
ids.binary_search(&2);   // pratique… mais Deref a TOUT exposé, y compris :
ids.push(0);             // ✕ [1, 2, 5, 0] : l'invariant « trié » est rompu, en silence.
// Deref sur un newtype rouvre la porte à ce que l'enveloppe devait protéger.

L'impl Deref semble offrir le meilleur des deux mondes : un type à soi et toutes les méthodes du Vec. Mais push, swap, sort_unstable_by remontent avec le reste, et n'importe lequel peut briser l'ordre que IdsTries promettait. Le type ne garantit plus rien : c'est un Vec avec un nom différent. Deref est fait pour les pointeurs intelligents (Box, Rc — Vol 6), pas pour simuler de l'héritage sur un newtype.The impl Deref seems to offer the best of both worlds: a type of your own and all of Vec's methods. But push, swap, sort_unstable_by come back with the rest, and any of them can break the order IdsTries promised. The type guarantees nothing anymore: it's a Vec with a different name. Deref is made for smart pointers (Box, Rc — Vol 6), not to fake inheritance on a newtype.

Le bon réflexe — déléguer ce qui préserve la garantieThe right reflex — delegate what preserves the guarantee
// On expose DÉLIBÉRÉMENT ce qui respecte l'invariant — délégation choisie
struct IdsTries(Vec<u32>);

impl IdsTries {
    fn inserer(&mut self, x: u32) {            // pas de push nu : insertion qui TRIE
        let pos = self.0.binary_search(&x).unwrap_or_else(|e| e);
        self.0.insert(pos, x);
    }
    fn contient(&self, x: u32) -> bool { self.0.binary_search(&x).is_ok() }
    fn as_slice(&self) -> &[u32] { &self.0 }   // lecture seule : sûre à exposer
}
// l'enveloppe ne fuit que ce qui préserve la garantie. l'invariant tient.

On choisit ce qui traverse l'enveloppe. inserer remplace push par une opération qui maintient le tri ; la lecture (contient, as_slice) est sûre, donc exposée librement. Le type tient enfin sa promesse — « toujours trié » devient un invariant que rien ne peut rompre depuis l'extérieur. Ré-exposer à la main est plus verbeux que Deref, mais c'est exactement le travail pour lequel on a créé le newtype : décider, opération par opération, ce qui est permis.You choose what crosses the wrapper. inserer replaces push with an operation that keeps the sort; reading (contient, as_slice) is safe, so freely exposed. The type finally keeps its promise — 'always sorted' becomes an invariant nothing can break from outside. Re-exposing by hand is wordier than Deref, but it's exactly the job the newtype was created for: deciding, operation by operation, what's allowed.

Newtype, ou extension trait ?Newtype, or extension trait?

Avant de dégainer un newtype, demande ce que tu veux vraiment. Implémenter un trait étranger sur un type étranger → newtype, c'est la seule voie. Juste ajouter des méthodes à un type que tu ne possèdes pas → pas besoin d'enveloppe : un extension trait (n°3) le fait sans rien emballer, en gardant le type d'origine. Le newtype impose un coût d'emballage à chaque usage ; ne le paie que si tu as besoin de l'impl, pas seulement des méthodes.Before reaching for a newtype, ask what you really want. Implementing a foreign trait on a foreign type → newtype, it's the only way. Just adding methods to a type you don't own → no wrapper needed: an extension trait (n°3) does it without wrapping anything, keeping the original type. The newtype imposes a wrapping cost at every use; pay it only if you need the impl, not merely the methods.

derive_more, contre la corvée.derive_more, against the chore.

Quand un newtype doit ré-exposer beaucoup de traits du type interne (Display, Add, Iterator…), les écrire à la main est fastidieux. La crate derive_more les dérive depuis le champ enveloppé, par annotation — comme thiserror le faisait pour les erreurs. C'est l'automatisation du boilerplate de délégation, sans rouvrir la porte que Deref aurait grande ouverte : tu choisis quels traits déléguer, un par un.When a newtype must re-expose many of the inner type's traits (Display, Add, Iterator…), writing them by hand is tedious. The derive_more crate derives them from the wrapped field, by annotation — as thiserror did for errors. It's delegation-boilerplate automation, without reopening the door Deref would have flung wide: you choose which traits to delegate, one by one.

Deref sur un newtype rend toutes les méthodes du type emballé — y compris celles que l'enveloppe devait interdire.Deref on a newtype hands back every method of the wrapped type — including the ones the wrapper was meant to forbid.
№ 05Bilan & éditoWrap-up & editorial2 min

La cohérence : une impl, une seule, partout — le prix de la confiance.Coherence: one impl, only one, everywhere — the price of trust.

Ce numéro clôt le Vol 5 par la contrainte qui rend tout le reste fiable. L'orphan rule limite où l'on peut implémenter — au moins l'un du trait ou du type doit être à soi — non par bureaucratie, mais pour garantir l'unicité globale des implémentations : la cohérence. De là découlent les patterns : le newtype, qui rend un type étranger « tien » pour débloquer une impl ; le blanket impl, qui dote tout un univers de types d'un comportement ; l'extension trait, qui ajoute des méthodes sans emballer. Et le piège, toujours le même refrain : ne pas défaire avec Deref la barrière qu'on vient d'ériger. Posséder, c'est avoir le droit d'implémenter — et la responsabilité de ne fuir que ce qui préserve la garantie.This issue closes Vol 5 with the constraint that makes everything else reliable. The orphan rule limits where you may implement — at least one of the trait or the type must be yours — not out of bureaucracy, but to guarantee implementations' global uniqueness: coherence. From it flow the patterns: the newtype, which makes a foreign type 'yours' to unlock an impl; the blanket impl, which endows a whole universe of types with a behavior; the extension trait, which adds methods without wrapping. And the trap, always the same refrain: don't undo with Deref the barrier you just raised. To own is to have the right to implement — and the responsibility to leak only what preserves the guarantee.

L'outilThe toolCe qu'il installeWhat it sets upLe réflexeThe reflex
Orphan ruleOrphan ruleAu plus une impl par (trait, type), dans tout le programme.At most one impl per (trait, type), in the whole program.Possède le trait OU le type, sinon l'impl est refusée.Own the trait OR the type, else the impl is refused.
NewtypeNewtypeUn type à soi autour d'un type étranger → impl permise.A type of your own around a foreign type → impl allowed.Trait + type tous deux étrangers → emballe dans un newtype.Trait + type both foreign → wrap in a newtype.
impl<T> … for TUn comportement pour tous les types d'une borne, d'un coup.A behavior for all types of a bound, at once.Comportement universel → blanket impl, mais sans exception.Universal behavior → blanket impl, but no exceptions.
Extension traitExtension traitDes méthodes sur un type étranger, sans l'emballer.Methods on a foreign type, without wrapping it.Juste des méthodes (pas une impl) → extension trait, pas newtype.Just methods (not an impl) → extension trait, not newtype.
L'orphan rule te limite pour que personne ne puisse changer en silence le comportement d'un type que tu utilises déjà.The orphan rule limits you so that no one can silently change the behavior of a type you already use.
Mot de l'éditeurFrom the editor

Cinq volumes, un même arbre. Le sixième montre comment en sortir.Five volumes, one same tree. The sixth shows how to leave it.

Reprends le fil. Le Vol 1 a posé la possession ; le Vol 2, les durées de vie ; le Vol 3, les types qui interdisent l'illégal ; le Vol 4, l'erreur comme valeur ; le Vol 5, le partage de comportement par les traits. Sous tout cela, une structure unique gouvernait la mémoire : l'arbre de possession. Un propriétaire par donnée, une racine, des branches qui se libèrent quand on les coupe. C'est ce qui rend la gestion mémoire de Rust automatique et sûre, sans ramasse-miettes. Mais l'arbre ne décrit pas tout le réel.Pick up the thread. Vol 1 set ownership; Vol 2, lifetimes; Vol 3, types that forbid the illegal; Vol 4, the error as a value; Vol 5, behavior sharing through traits. Beneath it all, one structure governed memory: the ownership tree. One owner per piece of data, a root, branches that free when cut. That's what makes Rust's memory management automatic and safe, without a garbage collector. But the tree doesn't describe all of reality.

Volume 6 : sortir de l'arbre de possession. Certaines structures ne sont pas des arbres — un graphe où deux nœuds pointent le même voisin, un cache partagé entre plusieurs propriétaires, une donnée qu'on doit muter à travers une référence partagée. L'arbre strict du Vol 1 ne suffit plus. Rust offre des échappatoires : Box pour déporter sur le tas, Rc et Arc pour plusieurs propriétaires, Cell et RefCell pour la mutabilité intérieure. Mais chacune a un prix — un comptage de références, une vérification d'emprunt repoussée à l'exécution. On quitte les garanties purement statiques pour un terrain où la sûreté se paie, parfois, en cycles. Le réflexe du volume : de combien de propriétaires ai-je besoin, et qu'est-ce que ça coûte ?Volume 6: leaving the ownership tree. Some structures aren't trees — a graph where two nodes point at the same neighbor, a cache shared between several owners, data you must mutate through a shared reference. Vol 1's strict tree no longer suffices. Rust offers escape hatches: Box to move onto the heap, Rc and Arc for multiple owners, Cell and RefCell for interior mutability. But each has a price — a reference count, a borrow check pushed to runtime. We leave the purely static guarantees for ground where safety is paid, sometimes, in cycles. The volume's reflex: how many owners do I need, and what does it cost?