Menu

Choice et match en Zero : unions étiquetées et filtrage par motifs

Comment choice déclare une union étiquetée en Zero et comment match branche de façon exhaustive sur ses variantes — la version Zero des types somme et du filtrage par motifs.

Cette page contient des éditeurs exécutables — modifiez, exécutez et voyez la sortie instantanément.

Unions étiquetées façon Zero

choice déclare un type dont la valeur est l'une de plusieurs variantes nommées, chaque variante portant sa propre charge utile :

choice Result {
    ok:  i32,
    err: String,
}

Une valeur Result est soit un ok portant un i32, soit un err portant une String. Jamais les deux, jamais aucun. Le système de types fait cette garantie, et match rend pratique d'agir dessus.

C'est la même idée que d'autres langages appellent « union étiquetée », « type somme », « union discriminée » ou « type de données algébrique ». Zero l'orthographie choice et garde la grammaire petite.

Déclarer un choice

choice Name {
    variantA: PayloadTypeA,
    variantB: PayloadTypeB,
}

Chaque ligne liste une variante. Le nom vous appartient ; le type après les deux points est la charge utile que cette variante porte. Une variante qui n'a pas besoin de charge utile utilise Void :

choice Token {
    word:   String,
    number: i32,
    eof:    Void,
}

Token.eof est une variante sans charge utile utile (son type de charge est Void) — utile pour les cas façon terminateur.

Construire une valeur de choice

Construisez une valeur en nommant le type, puis la variante, puis en passant la charge utile :

let success = Result.ok(42)
let failure = Result.err("validation failed")

Le type de la charge utile doit correspondre à celui déclaré pour la variante. Result.ok("hello") serait une erreur de compilation parce que ok attend un i32.

L'inférence de types fonctionne ici aussi. Si le côté droit détermine complètement le type, vous pouvez écrire let success = Result.ok(42) et le type de la liaison sera Result. Annoter le type explicitement est très bien quand vous voulez le documenter au site de liaison :

let success: Result = Result.ok(42)

Filtrer un choice

match est la façon de lire une valeur choice. La forme :

match value {
    .variantA => binding { /* corps quand value est variantA, avec la charge utile dans `binding` */ }
    .variantB => binding { /* corps quand value est variantB */ }
}

Un exemple complet tiré du dépôt officiel de Zero — cliquez sur Run pour voir la branche .ok se déclencher :

Lisez le match à la lettre : « selon la variante que result contient, exécute la branche correspondante et lie la charge utile au nom choisi ». Dans la branche .ok, value est la charge utile i32. Dans la branche .err, message est la charge utile String. Chaque branche est une portée distincte ; la liaison n'est visible qu'à l'intérieur de son propre corps.

Exhaustivité

C'est le grand atout de match sur les chaînes if/else if : le compilateur vérifie que chaque variante a une branche. Si vous oubliez le cas .err, vous ne tombez pas sur un fall-through à l'exécution vers une branche par défaut — vous obtenez une erreur de compilation :

{
    "code": "MAT001",
    "message": "match is not exhaustive: missing variant 'err'",
    "line": 9
}

(Le code d'erreur est illustratif ; le principe est le contrat.)

Ajoutez une nouvelle variante au choice — disons Result.timeout: Void — et chaque match sur Result dans la base de code devient une erreur de compilation tant que vous n'avez pas géré le nouveau cas. C'est une fonctionnalité, pas une corvée : le compilateur vous dit exactement où le nouveau cas a besoin d'attention.

Quand vous n'avez pas besoin de la charge utile

Si la charge utile d'une variante est Void ou que vous ne vous en souciez tout simplement pas dans cette branche, vous pouvez ignorer la liaison — mais vous devez quand même écrire la branche pour satisfaire l'exhaustivité :

match token {
    .word   => w { /* utiliser w */ }
    .number => n { /* utiliser n */ }
    .eof    => _ { /* rien à lier */ }
}

L'orthographe exacte pour « ignorer la charge utile » peut évoluer en Zero pré-1.0 (vous pourriez voir _ ou simplement omettre la liaison). Le point conceptuel — chaque variante a une branche, avec ou sans charge utile — est la partie stable.

Motifs courants

Un type d'erreur façon Result

C'est exactement l'exemple qu'utilise le dépôt officiel :

choice Result {
    ok:  i32,
    err: String,
}

Les fonctions qui peuvent réussir-avec-valeur ou échouer-avec-message retournent un Result. Les appelants filtrent par motifs pour extraire soit la valeur, soit le message. Le système raises/check de Zero gère la propagation des opérations faillibles ; Result est utile quand vous voulez garder une valeur succès-ou-échec sous forme de donnée.

Un token de parseur

choice Token {
    word:   String,
    number: i32,
    eof:    Void,
}

Un tokenizer produit un flux de Tokens. Chaque consommateur filtre sur la variante pour décider quoi faire — afficher le mot, sommer le nombre, sortir sur eof.

Une machine à états

choice State {
    waiting:    Void,
    processing: i32,
    done:       String,
}

processing porte l'ID de tâche en cours ; done porte le résultat final. Chaque transition est une nouvelle valeur State — pas de champs mutables éparpillés dans un shape.

Choice + génériques

choice peut être générique tout comme shape :

choice Maybe<T> {
    some: T,
    none: Void,
}

Maybe<i32> est « un entier optionnel ». Maybe<String> est « une chaîne optionnelle ». Ce même motif apparaît dans la bibliothèque standard de Zero et convient bien mieux qu'une sentinelle null — il n'y a pas moyen d'oublier le cas .none une fois que vous faites un match sur le type.

Quand utiliser choice vs. shape vs. enum

Petit récapitulatif des shapes et des enums :

  • Shape — enregistrement avec plusieurs champs, tous présents ensemble.
  • Enum — l'une de N étiquettes, sans données supplémentaires.
  • Choice — l'une de N variantes, chacune portant une charge utile.

La plupart des modèles de données dans un vrai programme sont une combinaison de ces trois-là. Le bénéfice de partir de « est-ce et, ou, ou ou avec données ? » est l'un des avantages sous-estimés du travail dans un petit langage.

La suite : la capacité World

choice et match couvrent le versant données de Zero. Le prochain chapitre porte sur les effets — comment les programmes Zero interagissent avec le monde extérieur. Il commence par la capacité World, l'objet qui contrôle chaque morceau d'E/S.

Questions fréquentes

Qu'est-ce qu'un choice en Zero ?

Un choice est le type union étiquetée de Zero — une valeur qui est l'une de plusieurs variantes nommées, chaque variante portant son propre type de charge utile. Exemple : choice Result { ok: i32, err: String }. Une valeur Result est soit un ok portant un i32, soit un err portant une String. Vous en construisez une avec Result.ok(42) ou Result.err("bad").

Comment fonctionne match en Zero ?

match value { .variantA => binding { ...body } .variantB => binding { ...body } } branche sur la variante que value contient. Chaque branche filtre une variante, nomme la liaison de la charge utile et exécute son corps. Le compilateur vérifie que vous avez couvert chaque variante — l'exhaustivité est le bénéfice phare par rapport à if/else if.

Comment construire une valeur de choice ?

Construisez-la en nommant le type et la variante, et en passant la charge utile : let r: Result = Result.ok(42) ou let r = Result.err("validation failed"). Le type de la charge utile doit correspondre à celui déclaré pour la variante — passer le mauvais type est une erreur de compilation.

Quelle est la différence entre choice et enum ?

Les variantes d'enum sont juste des étiquettes sans charge utile. Les variantes de choice portent chacune une valeur d'un type déclaré. Si vous avez besoin d'attacher des données à l'un des cas (un message d'erreur, un résultat réussi, un token parsé), utilisez choice. Si les cas sont des étiquettes pures, utilisez enum.

Pourquoi match est-il préféré à if/else pour les choices ?

match est exhaustif par construction — le compilateur vérifie que chaque variante est traitée, donc ajouter une nouvelle variante plus tard vous oblige à mettre à jour chaque site qui branche sur le type. Une chaîne if/else if passe silencieusement à côté, cachant le cas manquant jusqu'à ce qu'il apparaisse comme un bug en production.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER