Typy parametryczne odnoszą się do każdego typu, który może przyjmować parametr. Typów parametrycznych w Verse możesz używać, aby definiować uogólnione struktury danych i operacje. Typów parametrycznych można używać jako argumentów na dwa sposoby: albo w funkcjach jako argumentów typu jawnego lub niejawnego, albo w klasach jako argumentów typu jawnego.
Typowym przykładem typów parametrycznych są zdarzenia, których używa się bardzo często w urządzeniach w UEFN. Na przykład urządzenie przycisku ma zdarzenie InteractedWithEvent, które występuje za każdym razem, gdy gracz wchodzi w interakcję z przyciskiem. Aby zobaczyć typ parametryczny w działaniu, zapoznaj się ze zdarzeniem CountdownEndedEvent z samouczka Niestandardowy licznik czasu odliczania.
Argumenty typu jawnego
Wyobraź sobie pudełko (box), które przyjmuje dwa argumenty. Pozycja first_item inicjuje ItemOne, a second_item inicjuje ItemTwo, oba są typu type. Zarówno first_item, jak i second_item to przykłady typów parametrycznych, które są jawnymi argumentami dla klasy.
box(first_item:type, second_item:type) := class:
ItemOne:first_item
ItemTwo:second_itemPonieważ type jest argumentem typu dla first_item i second_item, klasę box można utworzyć z dowolnymi dwoma typami. Możesz mieć pudełko z dwiema wartościami string, pudełko z dwiema wartościami int, z wartością string oraz int, możesz mieć nawet pudełko z dwoma pudełkami!
Oto inny przykład: wyobraź sobie funkcję MakeOption(), która przyjmuje dowolny typ i zwraca option tego typu.
MakeOption(t:type):?t = false
IntOption := MakeOption(int)
FloatOption := MakeOption(float)
StringOption := MakeOption(string)Możesz zmodyfikować funkcję MakeOption(), aby zamiast tego zwracała dowolny typ kontenera, taki jak array lub map.
Argumenty typu niejawnego
Argumenty typu niejawnego dla funkcji wprowadza się za pomocą słowa kluczowego where. Przykładem może być funkcja ReturnItem(), która po prostu przyjmuje parametr i go zwraca:
ReturnItem(Item:t where t:type):t = ItemW tym przykładzie t to parametr typu niejawnego funkcji ReturnItem(), który przyjmuje argument typu type i od razu go zwraca. Typ t ogranicza, jaki typ Item możemy przekazać do tej funkcji. W tym przypadku, ponieważ t jest typu type, możemy wywołać ReturnItem() z dowolnym typem. Niejawnych typów parametrycznych używa się wraz z funkcjami, ponieważ umożliwiają pisanie kodu, który działa niezależnie od przekazanego do niego typu.
Na przykład zamiast zapisu:
ReturnInt(Item:int):int = Item
ReturnFloat(Item:float):float = ItemMożna napisać pojedynczą funkcję.
ReturnItem(Item:t where t:type):t = ItemTo gwarantuje, że ReturnItem() nie musi znać konkretnego typu t – niezależnie od wykonywanej operacji, będzie działało z każdym typem t.
Faktyczny typ zastosowany w odwołaniu do t będzie zależał od sposobu wykorzystania funkcji ReturnItem(). Jeśli na przykład funkcja ReturnItem() zostanie wywołana z argumentem 0.0, typem t będzie float.
ReturnItem("t") # t is a string
ReturnItem(0.0) # t is a floatW tym przykładzie "hello" oraz 0.0 to argumenty jawne (Item) przekazywane do ReturnItem(). Oba będą działały, ponieważ typem niejawnym Item jest t, które może być dowolnym typem type.
Jako inny przykład typu parametrycznego w charakterze argumentu niejawnego do funkcji rozważmy następującą funkcję MakeBox(), która operuje na klasie 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")
Funkcja MakeBox() przyjmuje dwa argumenty, FirstItemVal i SecondItemVal, oba typu type, i zwraca pudełko typu (type, type). Gdy w tym miejscu używamy type, mówimy MakeBox, że zwrócone pudełko może się składać z dowolnych dwóch obiektów, takich jak tablica, ciąg tekstowy, funkcja itp. Funkcja MakeBox() przekazuje oba argumenty do Box, używa ich, aby utworzyć pudełko, i je zwraca. Zauważ, że zarówno box, jak i MakeBox() używają takiej samej składni, jak wywołanie funkcji.
Wbudowanym przykładem jest funkcja dla typu kontenera Map pokazana poniżej.
Map(F(:t) : u, X : []t) : []u =
for (Y : X):
F(Y)Ograniczenia typu
Możesz określić ograniczenie typu wyrażenia. Obecnie jedynym obsługiwanym ograniczeniem jest podtyp mający zastosowanie wyłącznie do parametrów typu niejawnego. Na przykład:
int_box := class:
Item:int
MakeSubclassOfIntBox(NewBox:subtype_box where subtype_box:(subtype(int_box))) : tuple(subtype_box, int) = (NewBox, NewBox.Item)W tym przykładzie MakeSubclassOfIntBox() skompiluje się tylko wtedy, gdy zostanie do niej przekazana klasa będąca podklasą IntBox, ponieważ SubtypeBox ma typ (subtype(IntBox)). Zwróć uwagę, że type można traktować jako skrót subtype(any). Innymi słowy, funkcja ta akceptuje dowolny podtyp any, czyli każdy typ.
Kowariancja i kontrawariancja
Kowariancja i kontrawariancja odnoszą się do relacji dwóch typów, gdy typy te są używane w typach złożonych lub funkcjach. Dwa typy, które są powiązane jakiegoś rodzaju relacją, np. gdy jeden stanowi podklasę drugiego, są wzajemnie kowariancyjne lub kontrawariancyjne, zależnie od tego, w jaki sposób są używane w konkretnym fragmencie kodu.
Kowariancja: Używanie bardziej szczegółowego typu, gdy kod oczekuje czegoś bardziej ogólnego.
Kontrawariancja: Używanie bardziej ogólnego typu, gdy kod oczekuje czegoś bardziej szczegółowego.
Gdybyśmy na przykład mogli użyć int w sytuacji, gdy byłby zaakceptowany dowolny typ porównywalny comparable (taki jak float), nasz int działałby kowariancyjnie, ponieważ używamy bardziej szczegółowego typu niż oczekiwany bardziej ogólny. Przeciwnie, gdybyśmy mogli użyć dowolnego typu comparable, gdy normalnie użyłoby się int, nasz typ comparable działałby kontrawariancyjnie, ponieważ używamy typu bardziej ogólnego niż oczekiwany bardziej szczegółowy.
Przykład kowariancji i kontrawariancji w typie parametrycznym może być następujący:
MyFunction(Input:t where t:type):logic = trueW tym miejscu t jest używane kontrawariancyjnie jako dane wejściowe do funkcji, a logic jest używane kowariancyjnie jako dane wyjściowe funkcji.
Należy pamiętać, że oba typy nie są z natury kowariancyjne lub kontrawariancyjne względem siebie nawzajem. To, czy są kowariancyjne, czy kontrawariancyjne, zależy od sposobu ich użycia w kodzie.
Kowariant
Kowariancja oznacza używanie czegoś bardziej konkretnego, gdy oczekujemy czegoś ogólnego. Zwykle dotyczy to danych wyjściowych funkcji. Wszystkie użycia typu, które nie są danymi wejściowymi funkcji, są użyciami kowariancyjnymi.
Pokazany poniżej przykład ogólnego typu parametrycznego ma ładunek payload działający kowariancyjnie.
DoSomething():int =
payload:int = 0Wyobraźmy sobie na przykład, że mamy klasę animal i jej podklasę cat. Możemy też mieć klasę pet_sanctuary, która adoptuje zwierzęta przy użyciu funkcji AdoptPet(). Nie wiemy, jakie zwierzę dostaniemy, dlatego AdoptPet() zwraca ogólne animal.
animal := class:
cat := class(animal):
pet_sanctuary := class:
AdoptPet():animal = animal{}Wyobraźmy sobie inne schronisko, w którym są tylko koty. Klasa cat_sanctuary jest podklasą pet_sanctuary. Ponieważ schronisko jest tylko dla kotów, nadpisanie / zastępujemy AdoptPet() tak, aby zwracała tylko cat, a nie animal.
cat_sanctuary := class(pet_sanctuary):
AdoptPet<override>():cat = cat{}W tym przypadku zwracany typ cat funkcji AdoptPet() jest kowariancyjny dla animal. Używamy bardziej szczegółowego typu, a oryginał używał bardziej ogólnego.
Można to stosować również do typów złożonych. Gdy mamy tablicę cat, możemy zainicjować tablicę animal przy użyciu tablicy cat. Nie działa to w przeciwną stronę, ponieważ animal nie można skonwertować na jej podklasę cat. Tablica cat jest kowariancyjna względem tablicy animal, ponieważ traktujemy typ bardziej zawężony jako typ bardziej ogólny.
CatArray:[]cat = array{}
AnimalArray:[]animal = CatArrayDanych wejściowych do funkcji nie można używać kowariancyjnie. Poniższy kod nie będzie działał, ponieważ przypisanie AnimalExample() do CatExample() jest typu cat, co jest zbyt szczegółowe, aby być zwracanym typem AnimalExample(). Zadziała natomiast odwrócenie kolejności i przypisanie CatExample() do AnimalExample, ponieważ cat jest podtypem animal.
CatExample:type{CatFunction(MyCat:cat):void} = …
AnimalExample:type{AnimalFunction(MyAnimal:animal):void} = CatExampleNastępnie kolejny przykład, w którym zmienna t jest używana wyłącznie kowariancyjnie.
# The line below will fail because t is used only covariantly.
MyFunction(:logic where t:type):?t = falseKontrawariant
Kontrawariancja to przeciwieństwo kowariancji, oznacza używanie czegoś bardziej ogólnego, gdy oczekujemy czegoś konkretnego. Są to zwykle dane wejściowe funkcji. Pokazany poniżej przykład ogólnego typu parametrycznego ma ładunek payload działający kontrawariancyjnie.
DoSomething(Payload:payload where payload:type):voidPowiedzmy, że nasze schronisko dla zwierząt ma specjalną procedurę postępowania z nowymi kotami. Do pet_sanctuary dodajemy nową metodę o nazwie RegisterCat().
pet_sanctuary := class:
AdoptPet():animal = animal{}
RegisterCat(NewAnimal:cat):void = {}W naszym cat_sanctuary zastąpimy tę metodę, aby jako parametr typu przyjmowała animal, ponieważ wiemy już, że każdy cat to animal.
cat_sanctuary := class(pet_sanctuary):
AdoptPet<override>():cat = cat{}
RegisterCat<override>(NewAnimal:animal):void = {}W tym przykładzie animal jest kontrawariancyjne względem cat, ponieważ używamy czegoś bardziej ogólnego, chociaż działałoby coś bardziej szczegółowego.
Użycie typu niejawnego wprowadzonego przez klauzulę where kowariancyjnie generuje błąd. Na przykład w tym miejscu payload jest używany kontrawariancyjnie, ale powoduje błąd, ponieważ nie jest zdefiniowany jako argument.
DoSomething(:logic where payload:type) : ?payload = falseAby to naprawić, możemy przepisać kod, wyłączając parametr typu:
DoSomething(:logic) : ?false = falseUżycia wyłącznie kontrawariancyjne nie generują błędu, ale można je przepisać przy użyciu any zamiast false. Na przykład:
ReturnFirst(First:first_item, :second_item where first_item:type, second_item:type) : first_item = FirstPonieważ second_item był typu type i nie był zwrócony, możemy go zastąpić przez any w drugim przykładzie i uniknąć sprawdzania go pod kątem typu.
ReturnFirst(First:first_item, :any where first_item:type) : first_item = FirstZastąpienie typu first_item przez any lub false powoduje utratę precyzji. Na przykład poniższego kodu nie da się skompilować:
ReturnFirst(First:any, :any) :any = First
Main() : void =
FirstInt:int = ReturnFirst(1, "ignored")Znane ograniczenia
Parametrów typu jawnego dla typów danych można używać tylko z klasami, a nie z interfejsami lub strukturami. Niedozwolone jest też dziedziczenie odnoszące się do typów parametrycznych. | Verse |
Typy parametryczne mogą się odwoływać do siebie rekursywnie, o ile rekursja jest bezpośrednia. Typy parametryczne nie mogą odwoływać się rekursywnie do innych typów parametrycznych. | Verse |
Obecnie klasy obsługują wyłącznie niemodyfikowalne dane typów parametrycznych. Na przykład ten kod się nie skompiluje, ponieważ | Verse |
Parametry typu jawnego można swobodnie łączyć z klasą, podobnie jak parametry typu niejawnego można łączyć z funkcją. | Verse |