Génériques, traits et types nommés
Les génériques permettent d’écrire une logique pour plusieurs types. Les alias donnent des noms à des concepts existants, et les traits décrivent des capacités communes.
Nommer un type existant
Un alias introduit un nom parlant sans inventer une représentation différente. Par exemple :
type IdentifiantClient = u64;
proc charger_client(id: IdentifiantClient) -> bool {
return id > 0;
}
IdentifiantClient met en évidence la signification de la valeur dans cette API. L’alias reste associé au type qu’il désigne ; il n’apporte pas automatiquement une validation ou une protection contre les conversions accidentelles. Si une nouvelle valeur doit respecter des invariants propres, modélisez-la avec une forme ou un type opaque lorsque le compilateur prend en charge ce contrat.
La forme opaque Nom; déclare un type dont la représentation interne n’est pas exposée à ses utilisateurs. Elle permet de protéger une abstraction et de contrôler les opérations qui construisent ou inspectent la valeur. L’usage concret d’un type opaque dépend des fonctions que son module publie.
Une procédure générique
Un paramètre générique est un nom de type valable à l’intérieur d’une déclaration. Une fonction identité peut renvoyer exactement le type qu’elle reçoit :
proc identite<T>(valeur: T) -> T {
return valeur;
}
T n’est pas un type concret comme i64. C’est une variable de type : au moment de l’utilisation, le compilateur déduit ou vérifie quel type concret remplace T. Les génériques évitent d’écrire séparément la même procédure pour chaque type.
Les déclarations de formes et d’alias peuvent elles aussi avoir des paramètres génériques. Les bibliothèques du dépôt utilisent une notation entre crochets pour des applications de types comme Option[T], et une notation entre chevrons pour déclarer certains paramètres. Consultez la forme des modules locaux : le compilateur peut appliquer des conventions ou prendre en charge des sous-ensembles différents.
Contraindre un paramètre avec une capacité
Une procédure qui compare deux valeurs a besoin que leur type sache les comparer. Un trait sert à nommer ce type de capacité. La grammaire prévoit des déclarations de traits et des déclarations impl qui relient des types à des traits. Une clause where peut exprimer des contraintes de type associées à des génériques.
Un exemple de forme de clause, à lire comme une contrainte, est where T: Comparable. Cela dit que la procédure attend un type T qui satisfait le contrat nommé Comparable. Les traits du module core montrent des signatures de méthodes, comme Display pour écrire une représentation dans un formateur.
trait Affichable {
proc ecrire(&self, sortie: &mut Tampon) -> bool;
}
Les méthodes de trait peuvent employer &self pour recevoir une référence au récepteur. Ce style appartient au contrat de méthode. Avant d’implémenter un trait pour un type, examine la syntaxe utilisée par la version du compilateur et les modules fournis.
Les limites réelles de l’abstraction
La grammaire peut accepter une construction avant que toutes les étapes du compilateur puissent l’utiliser. La documentation de modules/core précise par exemple que l’intégration de génériques complexes et des implémentations de traits n’est pas encore complète dans le backend natif. Autrement dit, il existe une différence entre une forme reconnue, une forme comprise sémantiquement et une forme qui peut produire un exécutable.
examples/tutorials/generiques-limites.vit sert de cas témoin négatif pour une forme générique.
build/bin/vitte compile examples/tutorials/generiques-limites.vit --emit-c --quiet --no-color --diagnostic=jsonRésultat confirmé : code de sortie 1, diagnostic VITTE_BACKEND_E_CODEGEN. La déclaration ne doit donc pas être présentée comme compilable.
Pour apprendre un concept, observez les sources qui le déclarent ; pour planifier un projet, validez-le sur le compilateur effectivement installé. N’annoncez pas une capacité à partir de la seule présence d’un fichier de bibliothèque.
Quand une abstraction est utile
Écrivez d’abord une version simple. Si vous voyez plusieurs procédures presque identiques, repérez ce qui change et ce qui reste identique. Un générique convient quand la même logique marche réellement avec plusieurs types. Un trait convient quand des types différents partagent une opération avec le même sens. Un alias convient quand un nom de domaine améliore la lecture.
Ajouter plusieurs niveaux de types ou de traits avant d’avoir deux usages concrets rend l’API plus difficile à expliquer. Gardez l’abstraction minimale qui supprime la duplication observée.
Comparer les quatre façons de nommer ou généraliser
| Construction | Question à laquelle elle répond | Effet recherché |
|---|---|---|
type Alias = T | Quel nom métier rend T plus clair ici ? | Donner un nom supplémentaire au type existant. |
form | Quelles données doivent rester ensemble ? | Décrire une valeur composée de champs. |
opaque | Qui doit connaître la représentation interne ? | Masquer la représentation et contrôler les opérations publiques. |
| Générique | Quelle logique dépend d’un type sans dépendre d’un type concret ? | Exprimer une déclaration paramétrée par un type. |
trait | Quel comportement plusieurs types doivent-ils fournir ? | Nommer une capacité commune et son contrat. |
Ces idées peuvent se combiner, mais elles ne sont pas interchangeables. Un alias ne protège pas une valeur. Une forme ne devient pas générique parce qu’elle contient un champ typé. Un trait ne crée pas automatiquement un type commun ni une implémentation. Commencez par la question de conception, puis choisissez la construction minimale qui y répond.
Générique sans opération, contrainte avec opération
La procédure identité identite<T> reçoit puis renvoie une valeur de même type. Elle n’a pas besoin de connaître les opérations disponibles sur T : elle ne fait que transmettre la valeur. Une procédure qui calcule a + b a, elle, besoin d’une opération d’addition pour T. Le contrat générique doit exprimer cette capacité si la sémantique des traits est prise en charge par la version ciblée.
Ce raisonnement aide à éviter les contraintes trop fortes. Si le corps n’a besoin que de comparer deux valeurs, ne demandez pas en plus des capacités d’affichage ou de conversion. Chaque contrainte supplémentaire réduit le nombre de types qui peuvent satisfaire la procédure et rend l’interface plus lourde.
Pourquoi les limites de génération comptent
Une déclaration générique décrit une famille de cas. Pour produire le code final, le compilateur doit savoir comment résoudre le type concret et générer les opérations correspondantes. Dans l’état vérifié du backend natif, l’exemple de forme générique atteint la génération puis reçoit un diagnostic de codegen. Cela signifie que l’intention de l’API peut être expliquée, mais que le code ne doit pas être vendu comme un chemin exécutable dans cette configuration.
Lorsqu’une capacité évolue encore, il est utile de distinguer le modèle conceptuel de son implémentation disponible. La forme de l’interface et ses contraintes peuvent être étudiées dans les sources de core ; avant qu’une application dépende de cette abstraction, sa compilation doit être établie avec la version ciblée.
Évaluer une duplication avant de généraliser
Deux procédures spécialisées permettent de comparer directement les signatures et les corps. Si seul le type change, la duplication est candidate à une abstraction ; si la règle métier diffère, la séparation peut être justifiée. Si les corps diffèrent sur la règle métier, la duplication est peut-être légitime. Si seule une opération varie, demandez si le langage et son backend peuvent exprimer un contrat commun. Si le nom du type concret est la seule difficulté, un alias suffit.
Pour une identité, le paramètre générique est conceptuellement suffisant parce que le corps ne réalise aucune opération propre au type. Pour une comparaison ou une addition, il faut une capacité correspondante. Dans le dépôt audité, ces formes restent à distinguer de la génération runtime, qui n’est pas confirmée pour le backend natif.
Un paramètre de type est une variable de conception
Dans proc identite<T>(valeur: T) -> T, le même nom T relie le type de l’entrée à celui de la sortie. La procédure ne choisit pas un type particulier ; elle exprime une relation qui doit rester vraie pour chaque instanciation valide. Cette abstraction n’est utile que si le corps n’a pas besoin de propriétés particulières de T. L’identité peut transmettre une valeur sans comparer, additionner ou afficher son contenu.
Dès que le corps appelle une opération sur T, cette opération fait partie des exigences du paramètre. Une addition demande une capacité d’addition ; une comparaison demande une capacité de comparaison. Les contraintes telles que where T: Comparable nomment ce besoin dans la signature. Sans contrainte, une procédure générique ne devrait pas présumer qu’un type arbitraire possède les méthodes d’un type concret.
Traits : séparer la capacité de la représentation
Un trait décrit un ensemble d’opérations attendues sans imposer que les types compatibles partagent la même représentation mémoire. Une capacité d’affichage peut être implémentée par un entier, un point ou un type métier de manière différente, tant que chaque implémentation respecte le sens du contrat. La déclaration du trait définit l’interface ; une déclaration impl associe ensuite cette interface à un type.
Cette séparation facilite la réutilisation, mais augmente le travail du compilateur et les contraintes sémantiques. Le dépôt expose des traits dans core, tandis que la génération native de certaines formes génériques et implémentations reste limitée. Les fichiers de bibliothèque prouvent qu’un contrat est décrit ; ils ne prouvent pas que le backend sait instancier chaque combinaison de types. Le diagnostic VITTE_BACKEND_E_CODEGEN du cas négatif indique précisément un échec à la génération après les étapes antérieures.
Alias et type opaque protègent des objectifs différents
type IdentifiantClient = u64 ajoute un nom métier à un type existant. Il améliore la lecture des signatures, mais ne crée pas à lui seul une validation, une plage dédiée ou une distinction complète face aux autres u64. Si l’identité doit être renforcée, une forme ou un type opaque peut servir à contrôler la construction et l’accès.
Un type opaque cache sa représentation aux utilisateurs du module. Les opérations exposées deviennent alors les seules voies prévues pour créer ou manipuler la valeur. Un alias privilégie la simplicité et l’annotation du domaine ; un opaque privilégie l’encapsulation. Les génériques et traits généralisent respectivement un type d’entrée et des comportements ; une form rassemble des données. La différence entre ces constructions permet de choisir une abstraction selon le problème réel plutôt que d’accumuler les mécanismes.
Paramètres de type et vocabulaire des bibliothèques
Les fichiers de bibliothèque Vitte donnent des exemples d’applications de types comme Option[T]. Dans le code source, la déclaration générique et son utilisation peuvent suivre des conventions de délimiteurs différentes selon le contexte. Il est donc utile de lire ensemble la définition du type, son alias éventuel et les signatures qui l’emploient. Un mot ressemblant à un paramètre ne suffit pas à savoir s’il désigne un type concret ou une variable générique.
Cette lecture évite également de confondre un type générique avec une collection dynamique. Un paramètre T peut représenter une valeur de n’importe quel type admissible ; il ne fournit pas en lui-même une allocation, un parcours ou un stockage. Une collection doit définir ces comportements séparément, ainsi que ses capacités, son coût et la propriété de ses éléments.
Contraintes minimales et cohérence de l’interface
Une signature générique devrait annoncer les opérations nécessaires, sans exiger davantage. Si le corps ne fait que renvoyer T, un trait de comparaison n’apporte rien. Si la procédure trie des éléments, elle doit disposer d’un ordre cohérent. Une contrainte minimale élargit le nombre de types compatibles et documente les véritables hypothèses du calcul.
Le nom du trait doit exprimer le contrat sémantique : deux implémentations portant une méthode appelée compare peuvent encore trier de manière incompatible si l’une renverse l’ordre. Les tests et la documentation précisent le sens attendu. La prise en charge syntaxique de trait, impl et where ne suffit pas à établir cette cohérence ni la génération de chaque instanciation.