Vol. 3 · № 01 · Un atelier RustA Rust workshop

Enums & pattern matching, ou les sum types Enums & pattern matching, or sum types

Modéliser l'une de ces formes, exclusivement — et l'exhaustivité que match impose : le compilateur refuse d'oublier un cas. L'état invalide devient irreprésentable. Model one of these shapes, exclusively — and the exhaustiveness match enforces: the compiler refuses to forget a case. The invalid state becomes unrepresentable.

AudienceAudience
Dev ayant assimilé possession et lifetimes (Vols 1–2) Dev who has absorbed ownership and lifetimes (Vols 1–2)
Format
Self-paced
ChapitresChapters
5
Date
Juin 2026 Jun 2026
≈ 18 min ●●○○ TypesEnumsmatch
SommaireContents ·
№ 01CadrageFraming3 min

« L'une de ces formes, exclusivement » — inscrit dans le type.'One of these shapes, exclusively' — written into the type.

Les Vols 1 et 2 portaient sur la mémoire : qui possède, qui emprunte, pour combien de temps. Le Vol 3 change de registre — il porte sur les types, et sur une idée qui retourne la manière de concevoir un programme : au lieu de détecter les états invalides à l'exécution, on les rend impossibles à écrire. Le premier outil pour ça est l'enum : non pas une liste de constantes comme ailleurs, mais un type qui dit « une valeur est exactement l'une de ces formes ».Vols 1 and 2 were about memory: who owns, who borrows, for how long. Vol 3 shifts register — it's about types, and an idea that flips how you design a program: instead of detecting invalid states at runtime, you make them impossible to write. The first tool for it is the enum: not a list of constants as elsewhere, but a type that says 'a value is exactly one of these shapes'.

Un sum type — chaque variante a sa propre chargeA sum type — each variant carries its own payload
enum Paiement {
    Carte { numero: String, exp: String },   // chaque forme porte
    Virement { iban: String },                // SES propres données
    Especes,                                  // ou aucune
}

// une valeur Paiement est EXACTEMENT l'une des trois — jamais deux,
// jamais zéro, jamais un mélange. L'exclusivité est dans le type.
Sum type vs product typeSum type vs product type

Une struct est un produit : elle a un champ A et un champ B et un champ C — toutes les combinaisons existent. Un enum est une somme : la variante A ou B ou C — une seule à la fois. Là où le produit multiplie les états possibles, la somme les restreint à ceux qu'on a explicitement listés. C'est cette restriction qui fait disparaître l'invalide.A struct is a product: it has field A and field B and field C — all combinations exist. An enum is a sum: variant A or B or C — only one at a time. Where the product multiplies possible states, the sum restricts them to those you explicitly listed. That restriction is what makes the invalid disappear.

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

« Ces cas sont-ils mutuellement exclusifs ? » Si oui — un paiement est par carte ou par virement, jamais les deux — alors c'est un enum, pas une struct à champs optionnels. On modélise l'exclusivité dans le type, et le compilateur la fait respecter pour toi.'Are these cases mutually exclusive?' If yes — a payment is by card or by transfer, never both — then it's an enum, not a struct with optional fields. You model the exclusivity in the type, and the compiler enforces it for you.

Pas le enum que tu connais.Not the enum you know.

En C, Java ou TypeScript, un enum est une liste de noms — au fond, des entiers déguisés. L'enum de Rust est un type algébrique : chaque variante peut porter des données différentes, structurées. Carte transporte deux String, Virement un seul, Especes rien. C'est une union étiquetée, vérifiée par le compilateur — bien plus proche du « discriminated union » de TypeScript que de son enum.In C, Java or TypeScript, an enum is a list of names — at bottom, disguised integers. Rust's enum is an algebraic type: each variant can carry different, structured data. Carte carries two Strings, Virement one, Especes none. It's a tagged union, compiler-checked — far closer to TypeScript's 'discriminated union' than to its enum.

№ 02La surpriseThe surprise4 min

La « soupe de booléens » où l'état absurde est représentable.The 'boolean soup' where the absurd state is representable.

Venant d'un langage à objets, on modélise un état avec une struct à champs optionnels : un drapeau « en cours », des données « peut-être présentes », une erreur « peut-être là ». Chaque champ semble innocent. Mais leur produit fait exploser le nombre d'états — et la plupart n'ont aucun sens. Le bug n'est pas dans le code qui les manipule : il est dans le type, qui autorise l'absurde dès le départ.Coming from an OO language, you model a state with a struct of optional fields: an 'in progress' flag, 'maybe present' data, a 'maybe there' error. Each field looks innocent. But their product blows up the number of states — and most are meaningless. The bug isn't in the code handling them: it's in the type, which permits the absurd from the start.

Le produit des champs : 5 combinaisons sur 8 sont absurdesThe product of fields: 5 of 8 combinations are absurd
// TypeScript : un objet « état de requête » à champs optionnels
interface Requete {
  enCours: boolean;
  donnees?: Reponse;
  erreur?: string;
}

// états absurdes, tous représentables :
{ enCours: true,  donnees: x, erreur: "oops" }  // chargé ET en cours ET échoué ?!
{ enCours: false, donnees: undefined, erreur: undefined }  // ni l'un ni l'autre
// 2 × 2 × 2 = 8 combinaisons, dont 5 n'ont AUCUN sens
FFF✕ni l’un ni l’autreneither one nor the other
TFF✓en coursin progress
FTF✓réussiesucceeded
FFT✓échouéefailed
TTF✕en cours ET réussiein progress AND succeeded
TFT✕en cours ET échouéein progress AND failed
FTT✕réussie ET échouéesucceeded AND failed
TTT✕les trois à la foisall three at once
enCours donnees erreur  ·  3 valides, 5 absurdes mais représentables3 valid, 5 absurd yet representable

Trois champs indépendants, c'est 2³ = 8 états possibles — alors que la réalité n'en a que trois. Le code qui lit cette struct doit, partout, gérer (ou oublier de gérer) les cinq combinaisons impossibles : tester enCours, puis donnees, puis erreur, en espérant n'oublier aucun croisement. Chaque oubli est un bug en puissance, et rien dans le type ne t'en avertit.Three independent fields means 2³ = 8 possible states — when reality has only three. Code reading this struct must, everywhere, handle (or forget to handle) the five impossible combinations: test enCours, then donnees, then erreur, hoping to miss no crossing. Each miss is a latent bug, and nothing in the type warns you.

Une struct à champs optionnels autorise l'absurde et compte sur toi pour l'éviter. Un enum ne lui donne aucune écriture.A struct of optional fields permits the absurd and counts on you to avoid it. An enum gives it no spelling at all.
№ 03La mécaniqueThe mechanics5 min

Le match exhaustif : le compilateur refuse d'oublier un cas.Exhaustive match: the compiler refuses to forget a case.

L'enum borne les états ; le match force à tous les traiter. C'est la seconde moitié de la garantie, et la plus active : un match doit couvrir chaque variante, sans exception, ou le code ne compile pas. En prime, il déstructure — chaque branche lie les données portées par sa variante. Modéliser et traiter deviennent un seul geste, et l'oubli devient une erreur de compilation, pas un bug en production.The enum bounds the states; match forces you to handle them all. That's the second half of the guarantee, and the more active one: a match must cover every variant, no exception, or the code won't compile. As a bonus, it destructures — each arm binds the data its variant carries. Modeling and handling become a single gesture, and forgetting becomes a compile error, not a production bug.

Un match qui couvre tout — et lie les données au passageA match covering everything — binding the data along the way
fn afficher(r: &Requete) -> String {
    match r {
        Requete::EnCours          => "Chargement…".into(),
        Requete::Reussie(rep)     => format!("OK : {}", rep.corps),  // rep lié ici
        Requete::Echouee(msg)     => format!("Erreur : {msg}"),      // msg lié ici
    }
    // pas de branche par défaut : les trois cas sont couverts, point.
}

Chaque branche reçoit, déjà déstructurée, la donnée de sa variante : rep n'existe que dans la branche Reussie, msg que dans Echouee. Impossible de lire une donnée dans le mauvais état — le type l'a liée à son cas, le match la rend accessible exactement là où elle est garantie présente.Each arm receives, already destructured, its variant's data: rep exists only in the Reussie arm, msg only in Echouee. You can't read data in the wrong state — the type bound it to its case, and match makes it accessible exactly where it's guaranteed present.

Ajouter une variante casse tous les match — et c'est vouluAdding a variant breaks every match — and that's the point

// On ajoute une variante à l'enum…
enum Requete { EnCours, Reussie(Reponse), Echouee(String), Annulee }

fn afficher(r: &Requete) -> String {
    match r {
        Requete::EnCours      => "Chargement…".into(),
        Requete::Reussie(rep) => format!("OK : {}", rep.corps),
        Requete::Echouee(msg) => format!("Erreur : {msg}"),
    }   // ✕ error[E0004]: non-exhaustive patterns: `&Requete::Annulee` not covered
}       // le compilateur REFUSE de compiler tant qu'Annulee n'est pas traité

Ajoute Annulee à l'enum, et chaque match du programme cesse de compiler tant que le nouveau cas n'est pas traité. Dans un langage à switch permissif, le cas oublié filerait silencieusement dans le default ; ici, le compilateur te tend la liste exacte des endroits à mettre à jour. L'exhaustivité transforme une évolution risquée — « ai-je pensé à tous les endroits ? » — en une checklist générée par la machine.Add Annulee to the enum, and every match in the program stops compiling until the new case is handled. In a language with a permissive switch, the forgotten case would slip silently into the default; here, the compiler hands you the exact list of places to update. Exhaustiveness turns a risky change — 'did I think of every spot?' — into a machine-generated checklist.

L'enum dit quels états existent. Le match exhaustif garantit qu'aucun n'est oublié en silence — à l'écriture comme à l'évolution.The enum says which states exist. Exhaustive match guarantees none is silently forgotten — when writing and when evolving.
№ 04Le piègeThe trap4 min

Le joker _ => qui désactive l'exhaustivité.The _ => wildcard that switches off exhaustiveness.

Toute la garantie du match repose sur un point : le compilateur doit pouvoir vérifier que chaque variante est traitée. Le joker _ => casse précisément cette vérification. Il dit « tous les autres cas, pareil » — y compris les cas qui n'existent pas encore. Le jour où on ajoute une variante, elle tombe silencieusement dans le joker, et la garantie d'exhaustivité, qu'on croyait acquise, n'a jamais été là.The whole match guarantee rests on one thing: the compiler must be able to verify every variant is handled. The wildcard _ => breaks exactly that check. It says 'all other cases, the same' — including cases that don't exist yet. The day you add a variant, it falls silently into the wildcard, and the exhaustiveness guarantee you thought you had was never there.

Le contre-exemple — le joker avale la variante nouvelleThe counter-example — the wildcard swallows the new variant
fn est_terminal(r: &Requete) -> bool {
    match r {
        Requete::Reussie(_) => true,
        Requete::Echouee(_) => true,
        _ => false,                  // « tous les autres cas » — le joker
    }
}
// On ajoute Annulee (terminal !) à l'enum.
// Le match COMPILE toujours — Annulee tombe dans _ => false.
// Bug silencieux : une requête annulée est dite « non terminale ».

Le _ => false paraissait économe : pourquoi écrire EnCours quand « tout le reste » renvoie false ? Parce que « tout le reste » inclut aussi l'avenir. Annulee, ajoutée plus tard, est un état terminal — mais elle hérite du false du joker, sans un mot du compilateur. Le bug ne se voit ni à la compilation, ni à la relecture : il dort jusqu'à ce qu'une requête annulée soit traitée comme « encore en cours ».The _ => false looked thrifty: why write EnCours when 'all the rest' returns false? Because 'all the rest' also includes the future. Annulee, added later, is a terminal state — but it inherits the wildcard's false, without a word from the compiler. The bug shows neither at compile time nor on review: it sleeps until a cancelled request is treated as 'still in progress'.

Le bon réflexe — nommer chaque varianteThe right reflex — name every variant
fn est_terminal(r: &Requete) -> bool {
    match r {
        Requete::Reussie(_) => true,
        Requete::Echouee(_) => true,
        Requete::EnCours    => false,   // on nomme chaque cas
    }
}
// On ajoute Annulee. Le match NE COMPILE PLUS :
// error[E0004]: non-exhaustive patterns: `&Requete::Annulee` not covered
// → on est forcé de décider : Annulee est-elle terminale ? (oui)

En listant EnCours explicitement, on rend au compilateur son pouvoir : ajouter Annulee casse le match, et te force à trancher son cas en connaissance de cause. Le joker n'est pas interdit — il est légitime pour de vrais « tous les autres » (un u32 a quatre milliards de valeurs). Mais sur un enum métier dont les variantes vont évoluer, chaque _ => est une garantie d'exhaustivité qu'on abandonne volontairement.By listing EnCours explicitly, you give the compiler its power back: adding Annulee breaks the match, forcing you to decide its case knowingly. The wildcard isn't banned — it's legitimate for genuine 'everything else' (a u32 has four billion values). But on a domain enum whose variants will evolve, every _ => is an exhaustiveness guarantee you willingly give up.

Le juste milieuThe middle ground

Besoin de regrouper plusieurs cas sans tout réécrire ? Nomme-les par | : Reussie(_) | Echouee(_) => true. Tu gardes la concision sans abdiquer l'exhaustivité — le compilateur sait toujours quelles variantes sont couvertes, et te rappellera à l'ordre à la prochaine.Need to group several cases without rewriting all? Name them with |: Reussie(_) | Echouee(_) => true. You keep the concision without abdicating exhaustiveness — the compiler still knows which variants are covered, and will call you back on the next one.

Le test mentalThe mental test

Devant un _ => dans un match sur un enum métier : « si quelqu'un ajoute une variante demain, est-ce que je VEUX que le compilateur m'oblige à la traiter ici ? » Presque toujours oui. Réserve le joker aux types ouverts par nature (entiers, caractères) où nommer chaque valeur n'a aucun sens.Facing a _ => in a match on a domain enum: 'if someone adds a variant tomorrow, do I WANT the compiler to force me to handle it here?' Almost always yes. Reserve the wildcard for types open by nature (integers, chars) where naming every value makes no sense.

Chaque _ => sur un enum métier est une porte ouverte sur l'avenir : les variantes de demain y entreront sans frapper.Every _ => on a domain enum is a door left open to the future: tomorrow's variants will walk in unannounced.
№ 05Bilan & éditoWrap-up & editorial2 min

Ne pas vérifier l'invalide. Le rendre inexprimable.Don't check the invalid. Make it unwritable.

L'enum modélise « l'une de ces formes, exclusivement » ; le match force à toutes les traiter ; ensemble, ils font basculer une catégorie entière de bugs hors du domaine du possible. L'état absurde n'est pas attrapé par une validation : il n'a aucune écriture. C'est le même mouvement que le borrow checker du Vol 1 — non pas détecter une faute, mais retirer au langage le moyen de la commettre.The enum models 'one of these shapes, exclusively'; match forces you to handle them all; together they tip a whole class of bugs out of the realm of the possible. The absurd state isn't caught by a validation: it has no spelling. It's the same move as Vol 1's borrow checker — not detecting a fault, but denying the language the means to commit it.

L'outilThe toolCe qu'il garantitWhat it guaranteesLe réflexeThe reflex
enumUne valeur est exactement l'une des variantes listées — un sum type.A value is exactly one of the listed variants — a sum type.Cas exclusifs ? Enum, pas struct à champs optionnels.Mutually exclusive cases? Enum, not a struct of optional fields.
Charge par variantePer-variant payloadChaque variante porte sa donnée : impossible de la lire dans le mauvais état.Each variant carries its data: impossible to read it in the wrong state.Attache la donnée au cas où elle est garantie présente.Bind the data to the case where it's guaranteed present.
matchExhaustif : chaque variante traitée, ou refus de compiler (E0004).Exhaustive: every variant handled, or it won't compile (E0004).Ajouter une variante doit casser les match — c'est la checklist.Adding a variant should break the matches — that's the checklist.
_ =>Le joker désactive l'exhaustivité : les variantes futures y tombent.The wildcard disables exhaustiveness: future variants fall into it.À éviter sur un enum métier ; nomme les cas, ou groupe par |.Avoid on a domain enum; name the cases, or group with |.
« Rendre les états invalides irreprésentables » n'est pas un slogan : c'est la méthode de conception que le système de types met à ta portée.'Make invalid states unrepresentable' isn't a slogan: it's the design method the type system puts within your reach.
Mot de l'éditeurFrom the editor

Le bug à un milliard de dollars n'est pas une fatalité. C'est un choix de type.The billion-dollar mistake isn't a fate. It's a type choice.

Tony Hoare a appelé l'invention du null son « erreur à un milliard de dollars » : une valeur qui prétend être de n'importe quel type tout en n'en étant aucun, et qui fait planter les programmes depuis 1965. La plupart des langages l'ont héritée et tentent, depuis, de la rattraper — vérifications à l'exécution, opérateurs ?., analyses statiques greffées après coup. Rust, lui, ne l'a jamais introduite. Et il n'a eu besoin d'aucune fonctionnalité nouvelle pour ça : juste un enum.Tony Hoare called inventing null his 'billion-dollar mistake': a value claiming to be of any type while being none, crashing programs since 1965. Most languages inherited it and have been patching ever since — runtime checks, ?. operators, static analyses bolted on afterwards. Rust never introduced it. And it needed no new feature to manage that: just an enum.

Prochain numéro : Option. L'enum le plus important de tout Rust — deux variantes, Some(T) et None — qui remplace le null par une valeur que le compilateur t'oblige à ouvrir avant d'en lire le contenu. On verra comment « ce truc pourrait être absent » cesse d'être une note dans ta tête pour devenir une contrainte du type, et comment le match qu'on vient d'apprendre en est la clé.Next issue: Option. The single most important enum in all of Rust — two variants, Some(T) and None — replacing null with a value the compiler forces you to open before reading its contents. We'll see how 'this thing might be absent' stops being a note in your head and becomes a constraint of the type, and how the match we just learned is the key to it.