Tipi parametrici si riferisce a qualsiasi tipo che può assumere un parametro. Puoi utilizzare i tipi parametrici in Verse per definire strutture e operazioni di dati generalizzate. Esistono due modi per utilizzare i tipi parametrici come argomenti: in funzioni come argomenti di tipo esplicito o implicito oppure in classi come argomenti di tipo esplicito.
Gli eventi sono un tipico esempio di tipi parametrici e sono ampiamente utilizzati in tutti i dispositivi di UEFN. Ad esempio, il dispositivo Pulsante ha InteractedWithEvent che si verifica ogni volta che un giocatore interagisce con il pulsante. Per vedere un tipo parametrico in azione, guarda CountdownEndedEvent dal tutorial Timer del conto alla rovescia personalizzato.
Argomenti di tipo esplicito
Considera un box che prende due argomenti. Il first_item inizializza un ItemOne e il second_item inizializza un ItemTwo, entrambi di tipo type. Sia first_item che second_item sono esempi di tipi parametrici che sono argomenti espliciti per una classe.
box(first_item:type, second_item:type) := class:
ItemOne:first_item
ItemTwo:second_itemPoiché type è l'argomento del tipo per first_item e second_item, la classe box può essere creata con uno qualunque dei due tipi. Potresti avere un box di due valori string, un box di due valori int, un string e un int o perfino un box di due box!
Per un altro esempio, considera la funzione MakeOption(), che prende qualunque tipo e restituisce una option di quel tipo.
MakeOption(t:type):?t = false
IntOption := MakeOption(int)
FloatOption := MakeOption(float)
StringOption := MakeOption(string)Potresti modificare la funzione MakeOption() per restituire invece qualsiasi altro tipo di contenitore, come un array o una map.
Argomenti di tipo implicito
Gli argomenti di tipo implicito per le funzioni sono introdotti con la parola chiave where. Ad esempio, data una funzione ReturnItem() che prende semplicemente un parametro e lo restituisce:
ReturnItem(Item:t where t:type):t = ItemQui, t è un parametro di tipo implicito della funzione ReturnItem() che prende un argomento di tipo type e lo restituisce immediatamente. Il tipo di t limita il tipo di Item che possiamo passare a questa funzione. In questo caso, poiché t è di tipo type, possiamo chiamare ReturnItem() con qualsiasi tipo. Il motivo per utilizzare i tipi parametrici impliciti con le funzioni è che permette di scrivere un codice che funziona indipendentemente dal tipo ad esso trasmesso.
Ad esempio, invece di dover scrivere:
ReturnInt(Item:int):int = Item
ReturnFloat(Item:float):float = ItemSi potrebbe invece scrivere la singola funzione.
ReturnItem(Item:t where t:type):t = ItemCiò viene fornito con la garanzia che ReturnItem() non ha bisogno di sapere quale sia il tipo particolare t: a prescindere dall'operazione che esegue, funziona indipendentemente dal tipo di t.
Il tipo effettivo da utilizzare per t dipende dall'utilizzo di ReturnItem(). Ad esempio, se ReturnItem() viene chiamato con argomento 0.0, allora t è un float.
ReturnItem("t") # t is a string
ReturnItem(0.0) # t is a floatQui "hello" e 0.0 sono gli argomenti espliciti (l'Item) trasmessi a ReturnItem(). Entrambi funzioneranno perché il tipo implicito di Item è t che può essere di qualsiasi type.
Come altro esempio di tipo parametrico in quanto argomento implicito di una funzione, considera la seguente funzione MakeBox() che opera sulla 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")
Qui la funzione MakeBox() prende due argomenti, FirstItemVal e SecondItemVal, entrambi di tipo type e restituisce un box di tipo (type, type). Utilizzare type qui significa che stiamo dicendo a MakeBox che il box restituito potrebbe essere costituito da due oggetti qualsiasi: potrebbe essere un array, una stringa, una funzione ecc. La funzione MakeBox() trasmette entrambi gli argomenti a Box, li utilizza per creare un box e lo restituisce. Tieni presente che entrambi box e MakeBox() utilizzano la stessa sintassi come chiamata di funzione.
Un esempio integrato di questo è la funzione per il tipo di contenitore Map, fornito di seguito.
Map(F(:t) : u, X : []t) : []u =
for (Y : X):
F(Y)Vincoli di tipo
Puoi specificare un vincolo al tipo di espressione. L'unico vincolo attualmente supportato è il sottotipo e solo per parametri di tipo implicito. Per esempio:
int_box := class:
Item:int
MakeSubclassOfIntBox(NewBox:subtype_box where subtype_box:(subtype(int_box))) : tuple(subtype_box, int) = (NewBox, NewBox.Item)In questo esempio, MakeSubclassOfIntBox() compilerà solo una volta trasmessa una classe che è sottoclasse di IntBox, dato che SubtypeBox ha il tipo (subtype(IntBox)). Tieni presente che type può essere considerato un'abbreviazione di subtype(any). In altre parole, questa funzione accetta qualsiasi sottotipo di any che corrisponde a ogni tipo.
Covarianza e controvarianza
La covarianza e la controvarianza si riferiscono alla relazione tra due tipi quando i tipi sono utilizzati in tipi compositi o funzioni. Due tipi in qualche modo correlati, come quando uno è sottoclasse dell'altro, sono covarianti o controvarianti l'uno dell'altro in base a come sono utilizzati in un particolare tratto di codice.
Covariante: utilizzo di un tipo più specifico quando il codice si aspetta qualcosa di più generico.
Controvariante: utilizzo di un tipo più generico quando il codice si aspetta qualcosa di più specifico.
Per esempio, se potessimo utilizzare un int in una situazione in cui qualunque comparable sarebbe accettato (come un float), il nostro int agirebbe in modo covariante, dato che stiamo utilizzando un tipo più specifico quando ci si aspetta uno più generico. Al contrario, se potessimo utilizzare qualsiasi comparable quando normalmente sarebbe utilizzato un int, il nostro comparable agirebbe in modo controvariante, dato che stiamo utilizzando un tipo più generico quando ci si aspetta uno più specifico.
Un esempio di covarianza e controvarianza in un tipo parametrico potrebbe somigliare al seguente:
MyFunction(Input:t where t:type):logic = trueQui t è utilizzato in modo controvariante come input della funzione e logic è utilizzato in modo covariante come output della funzione.
È importante tenere a mente che i due tipi non sono intrinsecamente covarianti o controvarianti l'uno rispetto all'altro, piuttosto il loro agire come covarianti o controvarianti dipende da come vengono utilizzati nel codice.
Covariante
La covarianza consiste nell'utilizzare qualcosa di più specifico quando ci si aspetta qualcosa di generico. Solitamente si tratta dell'output di una funzione. Tutti gli utilizzi del tipo che non sono input delle funzioni sono di covariante.
Un esempio di tipo parametrico generico sotto ha payload che agisce in maniera covariante.
DoSomething():int =
payload:int = 0Per esempio, supponiamo di avere una classe animal e una classe cat sottoclasse di animal. Abbiamo anche una classe pet_sanctuary che adotta animali domestici con la funzione AdoptPet(). Poiché non sappiamo che genere di animale domestico stiamo per adottare, AdoptPet() restituisce un generico animal.
animal := class:
cat := class(animal):
pet_sanctuary := class:
AdoptPet():animal = animal{}Supponiamo di avere un altro rifugio di animali domestici che si occupa solo di gatti. Questa classe, cat_sanctuary, è una sottoclasse di pet_sanctuary. Poiché si tratta di un gattile, eseguiamo l'override di AdoptPet() per restituire solo un cat invece di un animal.
cat_sanctuary := class(pet_sanctuary):
AdoptPet<override>():cat = cat{}In questo caso, il tipo restituito cat di AdoptPet() è covariante rispetto a animal. Stiamo utilizzando un tipo più specifico quando l'originale ne ha utilizzato uno più generico.
Il discorso può valere anche per i tipi compositi. Dato un array di cat, possiamo inizializzare un array di animal utilizzando l'array di cat. L'operazione opposta non funziona dato che animal non può essere convertito nella sua sottoclasse cat. L'array di cat è covariante rispetto all'array di animal, perché stiamo trattando un tipo più ristretto come tipo più generico.
CatArray:[]cat = array{}
AnimalArray:[]animal = CatArrayNon è possibile usare gli input delle funzioni in maniera covariante. Il codice seguente non funziona perché l'assegnazione di AnimalExample(), a CatExample(), è di tipo cat, che è troppo specifica per essere il tipo restituito di AnimalExample(). Invertire questo ordine assegnando CatExample() a AnimalExample funzionerebbe essendo cat sottotipo di animal.
CatExample:type{CatFunction(MyCat:cat):void} = …
AnimalExample:type{AnimalFunction(MyAnimal:animal):void} = CatExampleSegue un ulteriore esempio in cui la variabile t è utilizzata solo in maniera covariante.
# The line below will fail because t is used only covariantly.
MyFunction(:logic where t:type):?t = falseControvariante
La controvarianza rappresenta l'opposto della covarianza e consiste nell'utilizzare qualcosa di più generico quando ci si aspetta qualcosa di specifico. Solitamente questo si tratta dell'input di una funzione. Un esempio di tipo parametrico generico sotto ha payload che agisce in maniera controvariante.
DoSomething(Payload:payload where payload:type):voidDiciamo che il nostro rifugio per animali domestici ha una procedura specifica per la gestione di gatti appena arrivati. Aggiungiamo un nuovo metodo a pet_sanctuary chiamato RegisterCat().
pet_sanctuary := class:
AdoptPet():animal = animal{}
RegisterCat(NewAnimal:cat):void = {}Per il nostro cat_sanctuary, eseguiremo l'override di questo metodo per accettare un animal come parametro del tipo perché già sappiamo che ogni cat è un animal.
cat_sanctuary := class(pet_sanctuary):
AdoptPet<override>():cat = cat{}
RegisterCat<override>(NewAnimal:animal):void = {}Qui animal è controvariante di cat, poiché stiamo utilizzando qualcosa di più generico quando funzionerebbe qualcosa di più specifico.
Utilizzando un tipo implicito introdotto da una clausola where, genera in maniera covariante un errore. Ad esempio, payload qui è utilizzato in modo controvariante, ma genera un errore per non essere definito come argomento.
DoSomething(:logic where payload:type) : ?payload = falsePer correggerlo, potrebbe essere riscritto per escludere un parametro del tipo:
DoSomething(:logic) : ?false = falseGli utilizzi solo di controvariante non restituiscono un errore, ma si possono riscrivere utilizzando any invece di false. Per esempio:
ReturnFirst(First:first_item, :second_item where first_item:type, second_item:type) : first_item = FirstPoiché second_item era di tipo type e non è stato restituito, possiamo sostituirlo con any nel secondo esempio ed evitare di fare il controllo del tipo.
ReturnFirst(First:first_item, :any where first_item:type) : first_item = FirstSostituendo il tipo first_item con any o false la precisione del codice sarà compromessa. Ad esempio, la compilazione di quanto segue non riesce:
ReturnFirst(First:any, :any) :any = First
Main() : void =
FirstInt:int = ReturnFirst(1, "ignored")Limitazioni note
Parametri di tipo esplicito dei tipi di dati si possono utilizzare solo con le classi e non con interfacce o strutture. Anche l'ereditarietà relativa al tipo parametrico non è permessa. | Verse |
I tipi parametrici possono fare riferimento a se stessi ricorsivamente purché la ricorsione sia diretta. I tipi parametrici non possono fare riferimento ricorsivamente ad altri tipi parametrici. | Verse |
Attualmente, le classi supportano solo dati di tipo parametrico immutabili. Per esempio, questo codice non compilerebbe perché | Verse |
I parametri di tipo esplicito si possono combinare liberamente con una classe, come i parametri di tipo implicito si possono combinare con una funzione. | Verse |