Formes et choix
Un bon modèle de données regroupe les valeurs qui vont ensemble et donne un nom à chaque état possible. En Vitte, les formes et les choix jouent ces rôles.
Regrouper des champs avec form
Une forme déclare un type composé. Elle rassemble plusieurs champs sous un seul nom :
form Point {
x: i64,
y: i64,
}
proc distance_manhattan(point: Point) -> i64 {
return point.x + point.y;
}
Le type Point évite de transporter séparément une coordonnée horizontale et une coordonnée verticale. Les noms des champs documentent leur rôle. Le corps peut y accéder avec la notation pointée, par exemple point.x.
On construit une valeur de forme en donnant les champs par leur nom :
proc exemple() -> i64 {
let point: Point = Point {
x: 12,
y: 8,
};
return distance_manhattan(point);
}
Cette construction rend l’ordre des données sans importance pour le lecteur et évite les appels remplis de nombres sans étiquette. Pour une forme imbriquée, un champ peut lui-même recevoir une valeur construite d’un autre type.
Les formes gardent ensemble des données cohérentes
Imaginez un rectangle défini par un coin minimum et un coin maximum. Deux champs de type Point expriment directement le modèle :
form Rectangle {
minimum: Point,
maximum: Point,
}
proc largeur(rectangle: Rectangle) -> i64 {
return rectangle.maximum.x - rectangle.minimum.x;
}
Quand plusieurs procédures reçoivent les mêmes groupes de paramètres, c’est souvent le signe qu’une forme pourrait donner un nom à ce groupe. Mais n’ajoute pas un nouveau type juste pour envelopper une valeur sans règle ni sens supplémentaire : un nom utile exprime une idée du domaine.
Représenter des états avec pick
Une déclaration pick énumère des variantes possibles. Choisir entre ces variantes signifie qu’une valeur est dans un état parmi plusieurs :
pick Etat {
Nouveau,
EnCours,
Termine,
}
proc est_actif(etat: Etat) -> bool {
return etat == Etat.EnCours;
}
Une variante peut également porter des types dans ses parenthèses selon la grammaire, comme Erreur(i64). Cette forme suggère qu’une variante peut contenir une information associée. Avant de baser un programme sur une variante porteuse de données, vérifiez les constructions et les accès réellement pris en charge dans le compilateur utilisé.
pickexamples/tutorials/formes-choix.vit additionne les champs d’un point et extrait le nombre porté par la variante Ok.
build/bin/vitte run examples/tutorials/formes-choix.vit --quiet --no-colorRésultat confirmé avec le frontend natif : code de sortie 0.
Choisir entre une forme et un choix
La distinction centrale est la suivante : est-ce que mon objet possède plusieurs propriétés en même temps, ou est-il dans un état parmi plusieurs ? Un compte a probablement un identifiant, un nom et un solde à la fois : c’est une bonne candidate pour une form. Une opération est soit en attente, soit réussie, soit échouée : c’est souvent un pick.
Un état explicite empêche des combinaisons invalides. Trois booléens séparés peuvent produire huit combinaisons, dont beaucoup n’ont aucun sens ; trois variantes bien nommées décrivent directement les cas autorisés.
Immutabilité et mise à jour
Un champ lu à travers une valeur ne signifie pas automatiquement que toute la forme peut être modifiée. La variable locale qui contient la forme doit elle-même être mutable pour les affectations acceptées par le langage, et l’analyseur doit autoriser la cible de l’affectation. Gardez des mises à jour explicites et utilisez des procédures de transformation lorsque vous souhaitez rendre un changement facile à vérifier.
Si un type se décrit facilement en une phrase avec ses champs, son nom clarifiera les signatures de toutes les procédures qui l’utilisent.
Commencer par les valeurs que le programme doit représenter
Quelques valeurs concrètes du domaine révèlent la structure appropriée. Pour un point, il y a toujours une coordonnée x et une coordonnée y : les deux champs coexistent, donc une form convient. Pour un résultat, l’opération est réussie ou échouée : les cas sont exclusifs, donc un pick convient.
Cette méthode évite deux erreurs fréquentes. Un sac de paramètres indépendants perd leur relation ; une forme contenant un champ pour chaque état possible produit beaucoup de champs inutiles. À l’inverse, une seule forme avec trois booléens autorise des combinaisons qui n’ont peut-être aucun sens. Le type doit refléter les états que le domaine autorise réellement.
Les champs n’imposent pas automatiquement leurs invariants
Une déclaration form Rectangle { minimum: Point, maximum: Point } garantit les deux emplacements et leurs types, mais elle ne dit pas que le point minimum se trouve effectivement avant le point maximum. Cette règle doit être contrôlée par une procédure de validation, une précondition ou une API qui construit la forme de manière contrôlée.
Quand une forme a plusieurs champs dépendants, notez son invariant en une phrase. Pour un rectangle : « pour chaque axe, la coordonnée minimum est inférieure ou égale à la coordonnée maximum ». Pour une ligne de panier : « le prix et la quantité sont non négatifs ». Ensuite, identifiez tous les endroits qui peuvent construire ou modifier cette forme.
Suivre une variante dans un match
Dans le cas vérifié Resultat::Ok(42), la valeur est construite avec sa variante et sa charge utile. Le bras Resultat::Ok(nombre) récupère le nombre ; le bras Resultat::Echec traite le cas sans donnée associée. Le nom qualifié rappelle à quel type appartient la variante.
Un bras par cas rend le traitement visible, même quand l’outil ne prouve pas l’exhaustivité de toutes les formes avancées. Conservez un retour de repli seulement s’il répond à une décision de conception explicite ; ne l’utilisez pas pour cacher un état que vous auriez oublié. Vérifiez la règle d’exhaustivité du compilateur ciblé avant de compter dessus.
Mettre à jour un objet sans cacher le changement
Quand un programme change un champ, demandez-vous si l’objet entier doit évoluer en place ou si une procédure devrait produire une nouvelle valeur. Dans les cas confirmés par le frontend natif, les tableaux mutables utilisent let mut et set. La mutabilité des champs de forme et le passage par référence dépendent des règles de la construction et du chemin du compilateur ; ne transposez pas une règle d’un type à un autre sans vérification.
Une opération nommée telle que valider_livre ou changer_etat rend souvent une règle métier plus claire qu’une série de modifications dispersées au travers des champs.
Un livre avec titre, pagination et auteur est une forme, car ses propriétés coexistent. Son état « disponible, prêté ou en réparation » est un choix, car une seule variante doit être active à la fois. Si l’état « prêté » nécessite une date de retour, placez cette donnée dans la charge utile de cette variante plutôt que dans un champ qui resterait valable pour les autres états.
Deux familles de types composés
Une form répond à la question « quelles propriétés existent ensemble ? ». Un Point contient les coordonnées x et y à la fois. Une procédure qui reçoit un Point déclare explicitement qu’elle travaille avec cette paire comme une seule valeur cohérente. Les champs nommés clarifient aussi les constructions : Point { x: 12, y: 8 } est plus explicite qu’une paire de nombres positionnels dont l’ordre doit être mémorisé.
Un pick répond à une autre question : « lequel de ces cas est actuellement vrai ? ». Une opération en attente, réussie ou échouée se trouve dans une variante à la fois. Trois booléens indépendants autoriseraient huit combinaisons, alors qu’un choix nomme directement les trois états attendus. Cela réduit l’espace des états représentables et rend les traitements par match plus faciles à expliquer.
Les données associées appartiennent à une variante
Une variante peut porter une charge utile. Resultat::Ok(42) associe une valeur au cas de réussite, tandis que Resultat::Echec peut exister sans donnée. Le bras correspondant de match reçoit la valeur associée à cette variante. Ce placement est important : un code d’erreur n’a pas à être renseigné lorsqu’une opération a réussi, et une valeur de résultat n’a pas à être inventée quand il y a échec.
La lecture d’une variante comporte deux opérations conceptuelles : identifier le cas actif, puis interpréter sa charge utile conformément à ce cas. Les valeurs qui ne sont pas compatibles avec la variante ne devraient pas être lues comme si elles y étaient. Le compilateur du dépôt prend en charge certains cas de pick avec charge utile jusqu’à l’exécution, mais les règles complètes d’exhaustivité et les formes génériques doivent être vérifiées sur la cible considérée.
Un type ne garantit que les contraintes qu’il exprime
Les champs d’une forme garantissent leur présence et leur type dans la structure déclarée ; ils ne garantissent pas automatiquement les relations métier entre eux. Un rectangle peut contenir deux points où le minimum dépasse le maximum. Un prix et une quantité peuvent être négatifs si leur type l’autorise. Ces règles appartiennent à un invariant, à une validation, à un contrat ou à une construction contrôlée.
Cette distinction guide l’API. Si la forme peut être construite librement depuis plusieurs modules, chaque appelant peut créer une valeur invalide. Si le module garde la représentation interne et expose des opérations de création qui vérifient les conditions, la construction peut devenir le point où l’invariant est établi. Les formes opaques et les limites de visibilité peuvent aider à protéger cette frontière lorsque leur usage est confirmé par le backend.
Lecture d’un match sur une valeur porteuse
La valeur Resultat::Ok(42) comporte à la fois le nom du type, la variante choisie et sa charge utile. Le motif Resultat::Ok(nombre) associe la donnée portée par cette variante au nom local nombre. La branche d’échec n’a pas à accéder à une charge utile qui n’existe pas. La provenance qualifiée de la variante permet également de distinguer deux types qui pourraient avoir un bras nommé Ok.
Le code de la branche doit respecter le type de retour de la procédure qui contient le match. Dans l’exemple, les deux cas renvoient un i64. Le compilateur doit traiter chacun des chemins pertinents ; cependant, les garanties d’exhaustivité ou certaines syntaxes de motifs peuvent varier entre analyseurs et backends. Le fichier vérifié lié dans la page établit un cas concret avec charge utile, pas la prise en charge de toutes les variantes possibles.
Encapsulation et invariants de construction
Une forme publique dont tous les champs peuvent être initialisés directement laisse aux appelants la responsabilité de ses invariants. Une API qui contrôle la construction peut vérifier une règle une fois et rendre la valeur disponible ensuite. Les mots de visibilité et les types opaques peuvent contribuer à cette séparation, mais ils ne dispensent pas de décrire ce que la procédure de construction garantit.
La décision dépend de l’importance de l’invariant. Une coordonnée Point peut généralement être construite directement. Un montant validé, une plage ou un état de transaction peut nécessiter un contrôle centralisé. L’abstraction aide lorsqu’elle empêche une classe de valeurs incohérentes ; elle devient inutile lorsque les consommateurs doivent encore revalider chacun de ses champs.