Types paramétriques désigne tout type pouvant prendre un paramètre. Vous pouvez utiliser des types paramétriques dans Verse pour définir des structures de données et des opérations généralisées. Vous pouvez utiliser les types paramétriques comme arguments de deux façons : soit dans des fonctions en tant qu'arguments de type explicite ou implicite, soit dans des classes en tant qu'arguments de type explicite.
Les événements sont un exemple courant de types paramétriques et sont largement utilisés dans les appareils de l'UEFN. Par exemple, l'appareil Bouton possède l'événement InteractedWithEvent, qui se produit chaque fois qu'un joueur appuie sur le bouton. Pour voir un type paramétrique en action, consultez la section CountdownEndedEvent du tutoriel Compte à rebours personnalisé.
Arguments de type explicite
Considérons une boîte qui prend deux arguments. Le first_item initialise un ItemOne, et le second_item initialise un ItemTwo, tous deux de type type. first_item et second_item sont des exemples de types paramétriques qui sont des arguments explicites d'une classe.
box(first_item:type, second_item:type) := class:
ItemOne:first_item
ItemTwo:second_itemÉtant donné que type est l'argument de type de first_item et second_item, il est possible de créer la classe box avec deux types quelconques. Vous pouvez avoir une case de deux valeurs string, une case de deux valeurs int, une string et une int, voire une case de deux cases.
Prenons comme autre exemple la fonction MakeOption(), qui prend n'importe quel type et renvoie une option de ce type.
MakeOption(t:type):?t = false
IntOption := MakeOption(int)
FloatOption := MakeOption(float)
StringOption := MakeOption(string)Vous pouvez modifier la fonction MakeOption() pour qu'elle renvoie à la place n'importe quel autre type de conteneur, comme array ou map.
Arguments de type implicite
Les arguments de type implicite pour les fonctions sont introduits à l'aide du mot-clé where. Par exemple, la fonction ReturnItem(), qui prend simplement un paramètre et le renvoie :
ReturnItem(Item:t where t:type):t = ItemEn l'occurrence, t est un paramètre de type implicite de la fonction ReturnItem(), qui prend un argument de type type et le renvoie immédiatement. Le type de t restreint le type de Item qu'il est possible de transmettre à cette fonction. Dans ce cas, puisque t est de type type, il est possible d'appeler ReturnItem() avec n'importe quel type. On utilise des types paramétriques implicites avec les fonctions pour pouvoir écrire des codes qui fonctionnent quel que soit le type qui leur est transmis.
Par exemple, au lieu de devoir écrire :
ReturnInt(Item:int):int = Item
ReturnFloat(Item:float):float = ItemLa fonction unique pourrait être écrite à la place.
ReturnItem(Item:t where t:type):t = ItemPar ailleurs, ReturnItem() n'a pas besoin de connaître le type particulier de t : quelle que soit l'opération qu'elle effectue, elle fonctionne indépendamment du type de t.
Le type réel à utiliser pour t dépend du mode d'utilisation de ReturnItem(). Par exemple, si ReturnItem() est appelé avec l'argument 0.0, t est un float.
ReturnItem("t") # t is a string
ReturnItem(0.0) # t is a floatIci, "hello" et 0.0 sont les arguments explicites (Item) transmis à ReturnItem(). Tous deux fonctionnent, car le type implicite de Item est t, qui peut être n'importe quel type.
Pour obtenir un autre exemple de type paramétrique utilisé comme argument implicite d'une fonction, considérez la fonction MakeBox() suivante qui opère sur la classe box.
box(first_item:type, second_item:type) := class:
ItemOne:first_item
ItemTwo:second_item
MakeBox(ItemOneVal:ValOne, SecondItemVal:ValTwo where ValOne:type, ValTwo:type):box(ValOne, ValTwo) =
box(ValOne, ValTwo){ItemOne := ItemOneVal, ItemTwo := SecondItemVal}
Main():void =
MakeBox("A", "B")
MakeBox(1, "B")
Ici, la fonction MakeBox() prend deux arguments, FirstItemVal et SecondItemVal, tous deux de type type, et renvoie une boîte de type (type, type). En utilisant type ici, vous indiquez à MakeBox que la boîte renvoyée peut être composée de deux objets quelconques ; il peut s'agir d'une matrice, d'une chaîne, d'une fonction, etc. La fonction MakeBox() transmet les deux arguments à Box, les utilise pour créer une boîte et la renvoie. Notez que box et MakeBox() utilisent la même syntaxe qu'un appel de fonction.
La fonction pour le type de conteneur Map, fournie ci-dessous, en est un exemple intégré.
Map(F(:t) : u, X : []t) : []u =
for (Y : X):
F(Y)Contraintes de type
Vous pouvez spécifier une contrainte sur le type d'une expression. La seule contrainte actuellement prise en charge est le sous-type, et uniquement pour les paramètres de type implicite. Par exemple :
int_box := class:
Item:int
MakeSubclassOfIntBox(NewBox:subtype_box where subtype_box:(subtype(int_box))) : tuple(subtype_box, int) = (NewBox, NewBox.Item)Dans cet exemple, MakeSubclassOfIntBox() n'est compilé que si vous lui transmettez une classe qui sous-classe IntBox étant donné que SubtypeBox a le type (subtype(IntBox)). Notez que type peut être considéré comme un raccourci de subtype(any). En d'autres termes, cette fonction accepte tout sous-type de any, c'est-à-dire n'importe quel type.
Covariance et contravariance
La covariance et la contravariance désignent la relation entre deux types lorsque ceux-ci sont utilisés dans des types composites ou des fonctions. Deux types liés d'une manière ou d'une autre, par exemple lorsqu'un type est sous-classé par l'autre, sont soit covariants, soit contravariants l'un par rapport à l'autre, en fonction de la manière dont ils sont utilisés dans un bout de code particulier.
Covariance : utilisation d'un type plus spécifique alors que le code attend quelque chose de plus générique.
Contravariance : utilisation d'un type plus général alors que le code attend quelque chose de plus spécifique.
Par exemple, si nous pouvions utiliser un int dans une situation où n'importe quel comparable serait accepté (comme un float), notre int agirait de manière covariante, car nous utilisons un type plus spécifique alors qu'un type plus générique est attendu. À l'inverse, si nous pouvions utiliser n'importe quel comparable là où normalement un int serait utilisé, notre comparable agirait de manière contravariante, car nous utilisons un type plus générique alors qu'un type plus spécifique est attendu.
Un exemple de covariance et de contravariance dans un type paramétrique pourrait ressembler à ce qui suit :
MyFunction(Input:t where t:type):logic = trueIci, t est utilisé de manière contravariante comme entrée de la fonction, et logic de manière covariante comme sortie de la fonction.
Il est important de garder à l'esprit que les deux types ne sont pas intrinsèquement covariants ou contravariants l'un par rapport à l'autre, mais que leur comportement covariant ou contravariant dépend de la manière dont ils sont utilisés dans le code.
Covariante
La covariance signifie que l'on utilise quelque chose de plus spécifique alors que l'on attend quelque chose de générique. Il s'agit généralement de la sortie d'une fonction. Toutes les utilisations de type qui ne sont pas des entrées de fonctions sont des utilisations covariantes.
Dans l'exemple de type paramétrique générique ci-dessous, payload agit de manière covariante.
DoSomething():int =
payload:int = 0Par exemple, supposons que nous ayons une classe animal, et une classe cat qui sous-classe la classe animal. Nous avons également une classe pet_sanctuary qui adopte des animaux avec la fonction AdoptPet(). Étant donné que nous ne savons pas quel type d'animal nous allons obtenir, AdoptPet() renvoie un animal générique.
animal := class:
cat := class(animal):
pet_sanctuary := class:
AdoptPet():animal = animal{}Supposons que nous ayons un autre refuge pour animaux de compagnie qui ne s'occupe que des chats. Cette classe, cat_sanctuary, est une sous-classe de pet_sanctuary. Puisqu'il s'agit d'un sanctuaire pour chats, nous remplaçons AdoptPet() pour ne renvoyer que cat au lieu d'un animal.
cat_sanctuary := class(pet_sanctuary):
AdoptPet<override>():cat = cat{}Dans ce cas, le type de renvoi cat de AdoptPet() est covariant à animal. Nous utilisons un type plus spécifique alors que l'original utilisait un type plus général.
Cela peut également s'appliquer aux types composites. Étant donné une matrice de cat, nous pouvons initialiser une matrice de animal en utilisant la matrice cat. L'inverse ne fonctionne pas, car animal ne peut pas être converti en sa sous-classe cat. La matrice de cat est covariante de la matrice de animal, car nous traitons un type plus spécifique comme un type plus générique.
CatArray:[]cat = array{}
AnimalArray:[]animal = CatArrayLes entrées des fonctions ne peuvent pas être utilisées de manière covariante. Le code suivant échoue, car l'assignation de AnimalExample(), à CatExample(), est de type cat, qui est trop spécifique pour être le type de retour de AnimalExample(). Il serait possible d'inverser cet ordre en assignant CatExample() à AnimalExample en raison du sous-typage de cat par rapport à animal.
CatExample:type{CatFunction(MyCat:cat):void} = …
AnimalExample:type{AnimalFunction(MyAnimal:animal):void} = CatExamplePar ailleurs, la variable t n'est utilisée que de manière covariante.
# The line below will fail because t is used only covariantly.
MyFunction(:logic where t:type):?t = falseContravariante
La contravariance est le contraire de la covariance et signifie que l'on utilise quelque chose de plus générique alors que l'on attend quelque chose de spécifique. Il s'agit généralement de l'entrée d'une fonction. Dans l'exemple de type paramétrique générique ci-dessous, payload agit de manière contravariante.
DoSomething(Payload:payload where payload:type):voidSupposons que notre refuge pour animaux dispose d'une procédure spécifique de prise en charge des nouveaux chats. Nous ajoutons une nouvelle méthode à pet_sanctuary appelée RegisterCat().
pet_sanctuary := class:
AdoptPet():animal = animal{}
RegisterCat(NewAnimal:cat):void = {}Pour notre cat_sanctuary, nous allons remplacer cette méthode pour accepter un animal comme type paramétrique, car nous savons déjà que tout cat est un animal.
cat_sanctuary := class(pet_sanctuary):
AdoptPet<override>():cat = cat{}
RegisterCat<override>(NewAnimal:animal):void = {}Ici, animal est contravariant de cat, car nous utilisons un terme plus générique alors qu'un terme plus spécifique conviendrait.
L'utilisation d'un type implicite introduit par une clause where de manière covariante produit une erreur. Par exemple, payload est utilisé ici de manière contravariante, mais génère des erreurs, car il n'est pas défini comme un argument.
DoSomething(:logic where payload:type) : ?payload = falsePour remédier à cela, ce code pourrait être réécrit de manière à exclure un type paramétrique :
DoSomething(:logic) : ?false = falseLes utilisations contravariantes uniquement n'entraînent pas d'erreur, mais vous pouvez les réécrire en utilisant any au lieu de false. Par exemple :
ReturnFirst(First:first_item, :second_item where first_item:type, second_item:type) : first_item = FirstÉtant donné que second_item était de type type et qu'il n'a pas été renvoyé, il est possible de le remplacer par any dans le second exemple et de ne pas en vérifier le type.
ReturnFirst(First:first_item, :any where first_item:type) : first_item = FirstLe remplacement du type first_item par any ou false entraîne une perte de précision. Par exemple, le code suivant ne sera pas compilé :
ReturnFirst(First:any, :any) :any = First
Main() : void =
FirstInt:int = ReturnFirst(1, "ignored")Limitations connues
Les paramètres de type explicites concernant les types de données ne peuvent être utilisés qu'avec des classes, et non avec des interfaces ou des structures. L'héritage lié au type paramétrique est également interdit. | Verse |
Les types paramétriques peuvent se référencer eux-mêmes de manière récursive, à condition que la récursion soit directe. Les types paramétriques ne peuvent pas faire référence de manière récursive à d'autres types paramétriques. | Verse |
Actuellement, les classes ne prennent en charge que les données paramétriques immuables. Par exemple, ce code n'est pas compilé, car | Verse |
|Les paramètres de type explicites peuvent être librement combinés avec une classe, tout comme les paramètres de type implicites peuvent être combinés avec une fonction.| | Verse |