Vol. 4 · № 02 · Un atelier RubyA Ruby workshop

freeze, ou rendre la mutation impossible freeze, or making mutation impossible

Si l'objet partagé est le danger du № 1, freeze en est la parade : un objet gelé refuse toute mutation par un FrozenError franc, donc se partage sans risque. Deux réserves — le gel est définitif, et superficiel : il ne gèle qu'un seul niveau. If the shared object is № 1's danger, freeze is its remedy: a frozen object refuses every mutation with a plain FrozenError, so it shares without risk. Two caveats — freezing is permanent, and shallow: it freezes one level only.

AudienceAudience
Dev qui a vu le piège de l'objet partagé et veut le neutraliser Dev who saw the shared-object trap and wants to neutralize it
Format
Self-paced
ChapitresChapters
5
Date
Mar 2026 Mar 2026
≈ 18 min ●●○○ freezeImmutabilitéFrozenError
SommaireContents ·
№ 01CadrageFraming3 min

L'autre réponse au partage : rendre l'objet impossible à muter.The other answer to sharing: make the object impossible to mutate.

Le numéro précédent montrait le danger : muter un objet partagé surprend tous ceux qui le tiennent. La parade y était « copie avant de muter ». freeze prend le problème par l'autre bout : au lieu de multiplier les copies, on gèle l'objet une fois pour toutes. Un objet gelé refuse toute mutation — un << ou un gsub! lève FrozenError au lieu de modifier en silence. Et si l'objet ne peut plus changer, le partager redevient parfaitement sûr : l'immutabilité, c'est l'aliasing sans le piège.The previous issue showed the danger: mutating a shared object surprises everyone who holds it. The remedy there was 'copy before mutating'. freeze takes the problem from the other end: instead of multiplying copies, you freeze the object once and for all. A frozen object refuses every mutation — a << or a gsub! raises FrozenError instead of modifying silently. And if the object can no longer change, sharing it becomes perfectly safe again: immutability is aliasing without the trap.

freeze : la mutation est refusée, pas silencieusefreeze: the mutation is refused, not silent
a = "ruby".freeze   # on gèle l'objet : la mutation devient impossible
b = a               # b et a désignent toujours le MÊME objet

a << " loop"
# FrozenError: can't modify frozen String: "ruby"
#   ↑ la mutation est REFUSÉE, pas silencieuse

a.frozen?   # => true
b.frozen?   # => true   un objet est gelé, pas une variable
Geler ferme la porte de la mutationFreezing shuts the mutation door
mutablemutable
objet librefree object
⟶
.freeze
immuableimmutable
objet geléfrozen object

freeze est à sens unique : on peut geler un objet, jamais le dégeler. À partir de là, toute tentative de mutation échoue bruyamment — l'erreur naît à la mutation fautive, pas douze frames plus loin.freeze is one-way: you can freeze an object, never thaw it. From then on, every mutation attempt fails loudly — the error is born at the offending mutation, not twelve frames later.

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

« Cet objet doit-il jamais changer après sa création ? Alors gèle-le, et partage-le sans crainte. » L'immutabilité transforme une donnée partagée d'un risque en une garantie : personne ne peut la muter dans ton dos, parce que personne ne le peut tout court."Should this object ever change after creation? Then freeze it, and share it without fear." Immutability turns shared data from a risk into a guarantee: no one can mutate it behind your back, because no one can mutate it at all.

On gèle un objet, pas un nom.You freeze an object, not a name.

Comme l'aliasing du № 1, freeze agit sur l'objet, jamais sur la variable. Geler par a gèle l'unique objet que b regarde aussi : b.frozen? devient true sans qu'on ait touché b. On peut toujours réassigner (a = "neuf" fait pointer a ailleurs) ; ce qu'on ne peut plus, c'est modifier l'objet gelé en place.Like the aliasing in № 1, freeze acts on the object, never on the variable. Freezing through a freezes the single object b looks at too: b.frozen? becomes true without touching b. You can still reassign (a = "new" points a elsewhere); what you can no longer do is modify the frozen object in place.

№ 02La surpriseThe surprise4 min

Une ligne de commentaire qui gèle toutes les chaînes du fichier.One comment line that freezes every string in the file.

Par défaut, chaque littéral de chaîne est un objet mutable, et réalloué à chaque évaluation — un coût qu'on paie sans le voir dans les boucles chaudes. Le commentaire magique # frozen_string_literal: true, en tête de fichier, change la donne : tous les littéraux de chaîne deviennent gelés ET partagés (un seul objet réutilisé). On gagne en allocations et en sûreté ; en échange, le code qui mutait un littéral lève maintenant FrozenError. La parade quand on a vraiment besoin d'une chaîne mutable : le + unaire, +"…", qui en redonne une copie libre.By default, every string literal is a mutable object, reallocated on each evaluation — a cost you pay unseen in hot loops. The magic comment # frozen_string_literal: true, at the top of the file, changes that: all string literals become frozen AND shared (one object reused). You gain on allocations and safety; in exchange, code that mutated a literal now raises FrozenError. The remedy when you genuinely need a mutable string: the unary +, +"…", which hands back a free copy.

Le défaut : littéraux mutables, réallouésThe default: mutable literals, reallocated
# Sans le commentaire magique (le défaut) : chaque littéral est MUTABLE
"ruby".frozen?        # => false

# et chaque "ruby" du fichier est un NOUVEL objet, réalloué à chaque passage
def tag = "ruby"
tag.object_id == tag.object_id   # => false   deux objets distincts
                                 #   une allocation à chaque appel

Chaque "ruby" du fichier crée un objet neuf, à chaque évaluation — pratique si on veut le muter ensuite, coûteux si on ne le mute jamais. Dans une méthode appelée des milliers de fois, c'est des milliers d'allocations pour une chaîne qu'on ne fait que lire.Each "ruby" in the file creates a fresh object, on every evaluation — handy if you mean to mutate it, costly if you never do. In a method called thousands of times, that's thousands of allocations for a string you only read.

Un littéral gelé n'est pas une contrainte : c'est un objet de moins à allouer, et un bug de moins à craindre.A frozen literal isn't a constraint: it's one less object to allocate, and one less bug to fear.
№ 03La mécaniqueThe mechanics5 min

freeze retourne l'objet, et ne gèle qu'un seul niveau.freeze returns the object, and freezes one level only.

Deux faits à connaître pour s'en servir juste. D'abord, freeze retourne l'objet lui-même — donc on l'enchaîne (CONST = [...].freeze), et frozen? répond à la question. Certains objets sont d'ailleurs gelés par nature : les Symbol, les Integer, nil, true et false sont immuables d'office, ce qui explique pourquoi les partager n'a jamais posé problème. Ensuite — et c'est le piège du prochain chapitre — freeze est superficiel : geler un tableau empêche d'y ajouter un élément, mais ne gèle pas les éléments déjà dedans.Two facts to use it well. First, freeze returns the object itself — so you chain it (CONST = [...].freeze), and frozen? answers the question. Some objects are frozen by nature anyway: Symbols, Integers, nil, true and false are immutable out of the box, which is why sharing them never caused trouble. Second — and it's the next chapter's trap — freeze is shallow: freezing an array stops you adding an element, but doesn't freeze the elements already inside.

freeze retourne l'objet ; certains sont gelés d'officefreeze returns the object; some are frozen by default
s = "ruby"
s.freeze          # => "ruby"   freeze RETOURNE l'objet (chaînable)
s.frozen?         # => true

# certains objets sont gelés par nature — immuables d'office
:ruby.frozen?     # => true   les Symbol, toujours
1.frozen?         # => true   les Integer, Float, nil, true, false
"ruby".frozen?    # => false  un littéral String, lui, est mutable (par défaut)
freeze est superficiel (shallow)freeze is shallow
# freeze est SUPERFICIEL : il gèle l'objet, pas son contenu
config = ["ruby"].freeze   # le tableau est gelé…
config << "loop"
# FrozenError: can't modify frozen Array

config[0] << " loop"       # …mais l'élément, lui, n'est PAS gelé
config[0]                  # => "ruby loop"   le contenu a bel et bien changé

# pour geler en profondeur, il faut geler chaque niveau soi-même
config.each(&:freeze)      # gèle aussi les éléments
Gelé en surface, vivant dessous.Frozen on top, alive underneath.

freeze ne touche que l'objet sur lequel on l'appelle. Geler un Array verrouille sa structure — on ne peut plus y pousser, retirer ou réordonner — mais chaque élément reste l'objet qu'il était, mutable s'il l'était. Un Hash gelé interdit d'ajouter une clé, pas de muter la valeur derrière une clé existante. Pour une vraie immutabilité profonde, on gèle récursivement, ou on n'imbrique que des objets déjà immuables.freeze only touches the object you call it on. Freezing an Array locks its structure — no more pushing, removing or reordering — but each element stays the object it was, mutable if it was. A frozen Hash forbids adding a key, not mutating the value behind an existing key. For true deep immutability, you freeze recursively, or nest only already-immutable objects.

Ce caractère superficiel n'est pas un défaut, c'est le pendant exact du dup du prochain numéro : freeze gèle un niveau, dup copie un niveau. Les deux opérations s'arrêtent à la première couche et laissent le contenu imbriqué tel quel. Retenir l'un, c'est déjà comprendre l'autre.This shallowness isn't a flaw, it's the exact mirror of the next issue's dup: freeze freezes one level, dup copies one level. Both operations stop at the first layer and leave the nested contents as they are. Grasp one and you already understand the other.

№ 04Le piègeThe trap4 min

Le .freeze qui ment : gelé en surface, partagé en profondeur.The .freeze that lies: frozen on top, shared underneath.

Le contre-exemple emblématique du numéro. On gèle une constante de configuration et on la croit, dès lors, sûre à partager partout. Mais freeze étant superficiel, ses valeurs imbriquées — un Array de tags, un Hash d'options — restent mutables et partagées. Muter l'une depuis n'importe où mute la constante elle-même, exactement le piège du № 1 — sauf qu'ici le .freeze a endormi la méfiance : on a vu le mot « frozen » et on a cessé de se demander qui d'autre tenait ce tableau. Le faux sentiment de sécurité coûte plus cher que pas de freeze du tout.The issue's emblematic counter-example. You freeze a configuration constant and, from then on, believe it safe to share everywhere. But freeze being shallow, its nested values — an Array of tags, a Hash of options — stay mutable and shared. Mutating one from anywhere mutates the constant itself, exactly the № 1 trap — except here the .freeze lulled your suspicion: you saw the word 'frozen' and stopped asking who else held that array. The false sense of security costs more than no freeze at all.

Le Hash est gelé, son :tags ne l'est pasThe Hash is frozen, its :tags isn't
# Une constante "gelée" qu'on croit donc sûre à partager
DEFAULTS = { tags: [], locale: "fr" }.freeze   # le Hash est gelé…

def build(opts = DEFAULTS)
  opts[:tags] << "new"   # …mais :tags est un Array NON gelé, partagé
  opts
end

build   # => { tags: ["new"], locale: "fr" }
build   # => { tags: ["new", "new"], locale: "fr" }
        #   ↑ DEFAULTS[:tags] gonfle : le .freeze a MENTI sur la sûreté
Geler chaque niveau, ou ne jamais muter le partagéFreeze every level, or never mutate the shared
# Option A — geler en profondeur : plus rien n'est mutable
DEFAULTS = { tags: [].freeze, locale: "fr" }.freeze
DEFAULTS[:tags] << "new"
# FrozenError: can't modify frozen Array   ← l'erreur naît à la faute

# Option B — ne jamais muter le partagé : produire du neuf
def build(opts = DEFAULTS)
  { **opts, tags: opts[:tags] + ["new"] }   # + crée un nouvel Array
end                                          # DEFAULTS reste intact
Le contre-exemple obligatoire.The mandatory counter-example.

Un .freeze au bout d'un littéral imbriqué ne gèle que la coquille. Deux issues honnêtes. Geler en profondeur : chaque niveau reçoit son freeze, et toute mutation lève alors FrozenError à sa cause, au lieu de polluer en silence. Ne pas muter le partagé : on retombe sur le réflexe du № 1 — + et ** produisent du neuf, la constante reste intacte. Ce qu'on ne fait jamais : geler la coquille et faire confiance au mot « frozen » sans regarder dessous.A .freeze at the end of a nested literal freezes only the shell. Two honest ways out. Freeze deeply: each level gets its freeze, and any mutation then raises FrozenError at its cause, instead of polluting silently. Don't mutate the shared: back to the № 1 reflex — + and ** produce something new, the constant stays intact. What you never do: freeze the shell and trust the word 'frozen' without looking underneath.

« Gelé » sur la coquille ne dit rien du contenu. Un .freeze superficiel rassure sans protéger — le pire des deux mondes."Frozen" on the shell says nothing about the contents. A shallow .freeze reassures without protecting — the worst of both worlds.
№ 05Bilan & éditoWrap-up & editorial2 min

Geler ce qui ne doit pas changer — et savoir jusqu'où ça gèle.Freeze what mustn't change — and know how far the freeze reaches.

freeze est la réponse positive au piège du № 1 : si l'objet ne peut plus muter, le partager est sans risque. Un objet gelé refuse toute mutation par un FrozenError franc, plutôt qu'une corruption silencieuse à distance. Deux réserves à garder en tête : le gel est définitif (on ne dégèle pas), et il est superficiel (un seul niveau). Geler la coquille en croyant geler le contenu est pire que ne rien geler — ça rassure sans protéger.freeze is the positive answer to the № 1 trap: if the object can no longer mutate, sharing it carries no risk. A frozen object refuses every mutation with a plain FrozenError, rather than silent corruption at a distance. Two caveats to keep in mind: freezing is permanent (no thawing), and it's shallow (one level only). Freezing the shell while believing you froze the contents is worse than freezing nothing — it reassures without protecting.

La questionThe questionL'outilThe toolLe réflexeThe reflex
Ne doit jamais changer ?Must never change?obj.freeze — la mutation lèvera FrozenError.obj.freeze — mutation will raise FrozenError.Gèle-le, et partage-le sans crainte.Freeze it, and share it without fear.
Des littéraux en lecture seuleRead-only literals# frozen_string_literal: true en tête de fichier.# frozen_string_literal: true at the top of the file.Moins d'allocations ; +"…" si tu en veux une mutable.Fewer allocations; +"…" if you want a mutable one.
Gelé ?Frozen?obj.frozen? — Symbol, Integer, nil sont gelés d'office.obj.frozen? — Symbol, Integer, nil are frozen by default.Doute sur la sûreté du partage ? demande-lui.Doubt about sharing safety? ask it.
Une structure imbriquéeA nested structurefreeze est superficiel : un seul niveau gelé.freeze is shallow: one level frozen.Gèle chaque niveau, ou ne mute jamais le contenu.Freeze every level, or never mutate the contents.
Un objet gelé est un objet en qui on peut avoir confiance sans le surveiller — à condition de savoir qu'il ne gèle qu'une couche.A frozen object is one you can trust without watching — provided you know it freezes only one layer.
Mot de l'éditeurFrom the editor

Deux verbes pour le même mot : un niveau.Two verbs for the same word: one level.

Le Vol 4 tourne autour d'une seule vérité — en Ruby, on manipule des objets partagés, pas des valeurs copiées — et chaque numéro en éclaire une face. Le № 1 a nommé le danger : muter à distance. Celui-ci nomme la parade par le gel : rendre l'objet incapable de changer, donc sûr à partager. On a aussi rencontré, sans le creuser encore, le mot qui va structurer la suite : superficiel. freeze ne gèle qu'une couche, comme on le verra, dup n'en copie qu'une. Le modèle objet de Ruby travaille toujours au premier niveau ; le programmeur, lui, doit décider quoi faire des niveaux d'en dessous.Vol 4 circles one truth — in Ruby you manipulate shared objects, not copied values — and each issue lights up a face of it. № 1 named the danger: mutating at a distance. This one names the freeze remedy: making the object unable to change, hence safe to share. We also met, without digging in yet, the word that will shape what follows: shallow. freeze freezes only one layer, and as we'll see, dup copies only one. Ruby's object model always works at the first level; the programmer must decide what to do with the levels beneath.

Prochain numéro : dup vs clone & la copie de surface. On a gelé un niveau ; maintenant on en copie un. Et le piège est le miroir exact de celui-ci : dup fait une copie superficielle, ses sous-objets restent partagés avec l'original. On verra pourquoi dup et clone diffèrent (le gel, justement, et le singleton), et quand une copie d'un seul niveau ne suffit pas.Next issue: dup vs clone & shallow copy. We froze one level; now we copy one. And the trap is the exact mirror of this one: dup makes a shallow copy, its sub-objects stay shared with the original. We'll see why dup and clone differ (freezing, precisely, and the singleton), and when a one-level copy isn't enough.