Параметрический тип — это любой тип, который может принимать параметр. Такие типы можно использовать в Verse для определения обобщённых структур данных и операций. Есть два способа использовать эти типы в качестве аргументов: в функциях как аргументы явного или неявного типа или в классах как аргументы явного типа.
Типичным примером параметрических типов являются события, которые активно используются в устройствах в UEFN. Например, устройство «Кнопка» имеет событие InteractedWithEvent, которое происходит всякий раз, когда игрок взаимодействует с кнопкой. Чтобы увидеть параметрический тип в действии, ознакомьтесь с событием CountdownEndedEvent в уроке Настраиваемый таймер обратного отсчёта.
Аргументы явного типа
Рассмотрим класс box, который принимает два аргумента. Первый из них, first_item, инициализирует ItemOne, а второй, second_item, — ItemTwo, и оба имеют тип type. И first_item, и second_item — это примеры параметрических типов, которые являются явными аргументами класса.
box(first_item:type, second_item:type) := class:
ItemOne:first_item
ItemTwo:second_itemПоскольку type — это аргумент типа для first_item и second_item, класс box может быть создан с двумя любыми типами. Вы можете создать экземпляр box с двумя значениями string, с двумя значениями int, со значениями string и int или даже с двумя значениями типа box!
Ещё один пример: пусть есть функция MakeOption(), которая принимает любой тип и возвращает option этого типа.
MakeOption(t:type):?t = false
IntOption := MakeOption(int)
FloatOption := MakeOption(float)
StringOption := MakeOption(string)Вы можете изменить функцию MakeOption() так, чтобы она возвращала любой другой контейнерный тип, к примеру array или map.
Аргументы неявного типа
Аргументы неявного типа для функций вводятся с помощью ключевого слова where. К примеру, пусть есть функция ReturnItem(), которая просто принимает параметр и возвращает его:
ReturnItem(Item:t where t:type):t = Itemt является параметром неявного типа функции ReturnItem(), которая принимает аргумент типа type и сразу же возвращает его. Тип t ограничивает тип аргумента Item, который можно передавать в эту функцию. В данном случае мы можем вызвать ReturnItem() с любым типом, поскольку t имеет тип type. Преимущество использования неявных параметрических типов с функциями заключается в том, что это позволяет писать универсальный код, работающий вне зависимости от переданного типа.
Например, вместо двух функций
ReturnInt(Item:int):int = Item
ReturnFloat(Item:float):float = Itemможно написать одну функцию
ReturnItem(Item:t where t:type):t = ItemБлагодаря этому функции ReturnItem() нет необходимости знать, каким конкретно типом является t; какая бы операция не выполнялась, она выполнится вне зависимости от типа t.
Фактический тип, который будет использоваться в качестве t, зависит от того, как используется ReturnItem(). К примеру, если ReturnItem() вызывается с аргументом 0,0, то t будет иметь тип float.
ReturnItem("t") # t is a string
ReturnItem(0.0) # t is a floatЗдесь "hello" и 0,0 являются явными аргументами (Item), переданными в функцию ReturnItem(). Оба аргумента допустимы, поскольку неявный тип Item — это t, который может иметь любой тип.
Ещё один пример использования параметрического типа в качестве неявного аргумента функции — это следующая функция MakeBox(), которая работает с классом 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")
В данном случае функция MakeBox() принимает два аргумента, FirstItemVal и SecondItemVal, оба типа type, и возвращает box типа (type, type). Использование type означает, что мы сообщаем MakeBox, что возвращаемый экземпляр box может состоять из любых двух объектов; это могут быть массивы, строки, функции и т. д. Функция MakeBox() передаёт оба аргумента в box, использует их для создания экземпляра box и возвращает этот экземпляр. Обратите внимание, что и box, и MakeBox() используют один и тот же синтаксис вызова функции.
Этот принцип иллюстрирует встроенная функция для контейнерного типа map, представленная ниже.
Map(F(:t) : u, X : []t) : []u =
for (Y : X):
F(Y)Ограничения типов
Вы можете ограничить тип выражения. Единственное поддерживаемое на данный момент ограничение — это подтип (работает только с параметрами неявного типа). Пример:
int_box := class:
Item:int
MakeSubclassOfIntBox(NewBox:subtype_box where subtype_box:(subtype(int_box))) : tuple(subtype_box, int) = (NewBox, NewBox.Item)В этом примере MakeSubclassOfIntBox() скомпилируется только при передаче класса, который является подклассом IntBox, поскольку SubtypeBox имеет тип (subtype(IntBox)). Обратите внимание, что type можно рассматривать в качестве сокращения от subtype(any). Другими словами, эта функция принимает любой подтип any, который может быть любым типом.
Ковариантность и контрвариантность
Термины «ковариантность» и «контрвариантность» описывают взаимосвязь двух типов, когда они используются в составных типах или функциях. Два типа, которые связаны каким-либо образом, например когда один является подклассом другого, являются либо ковариантными, либо контрвариантными по отношению друг к другу в зависимости от того, как они используются в конкретном фрагменте кода.
Ковариантность: использование более конкретного типа, когда код ожидает более универсальный тип.
Контравариантность: использование более универсального типа, когда код ожидает более конкретный тип.
К примеру, если бы мы могли использовать тип int в ситуации, когда, допустим, любой тип comparable (например, float), тип int вёл бы себя ковариантно, поскольку мы бы использовали более конкретный тип, когда ожидался более универсальный. И наоборот, если бы мы могли использовать любой тип comparable в ситуации, когда обычно используется int, тип comparable действовал бы контрвариантно, поскольку мы бы использовали более универсальный тип, когда ожидается более конкретный.
Пример ковариантности и контрвариантности в параметрическом типе:
MyFunction(Input:t where t:type):logic = trueВ этом примере t используется контрвариантно в качестве типа аргумента функции, а logic — ковариантно в качестве типа возвращаемых данных функции.
Важно иметь в виду, что эти два типа не являются ковариантными или контравариантными по отношению друг к другу по своей сути — они выступают в качестве ковариантных или контравариантных в зависимости от того, как используются в коде.
Ковариантность
Ковариантность означает использование чего-то более конкретного, когда ожидается что-то более универсальное. Как правило, это возвращаемые данные функции. Все способы использования типов, когда это не типы возвращаемых данных функции, являются ковариантным использованием.
В следующем примере универсальный параметрический тип payload используется ковариантно.
DoSomething():int =
payload:int = 0К примеру, представим, что у нас есть класс animal и класс cat, который является подклассом animal. У нас также есть класс pet_sanctuary, который принимает питомцев с помощью функции AdoptPet(). Поскольку мы не знаем, питомца какого вида получим, AdoptPet() возвращает универсальное значение animal.
animal := class:
cat := class(animal):
pet_sanctuary := class:
AdoptPet():animal = animal{}Предположим, что у нас есть другой приют, в котором живут только кошки. Этот класс cat_sanctuary является подклассом pet_sanctuary. Поскольку это приют для кошек, мы переопределяем AdoptPet() таким образом, чтобы эта функция возвращала только cat вместо animal.
cat_sanctuary := class(pet_sanctuary):
AdoptPet<override>():cat = cat{}В этом случае возвращаемый тип cat функции AdoptPet() является ковариантным по отношению к animal. Мы используем более конкретный тип, тогда как исходная функция использует более универсальный.
Это также относится к составным типам. При наличии массива cat мы можем инициализировать массив animal массивом cat. В обратную сторону это не работает, поскольку animal нельзя преобразовать в его подкласс cat. Массив cat является ковариантным по отношению к массиву animal, поскольку мы рассматриваем более конкретный тип как более универсальный.
CatArray:[]cat = array{}
AnimalArray:[]animal = CatArrayАргументы функции нельзя использовать ковариантно. Следующий код не будет выполняться из-за присваивания функции AnimalExample() значения CatExample() типа cat, который является слишком конкретным по отношению к возвращаемому типу AnimalExample(). Если поменять порядок и присвоить CatExample() значение AnimalExample, то код будет выполняться, поскольку cat является подтипом animal.
CatExample:type{CatFunction(MyCat:cat):void} = …
AnimalExample:type{AnimalFunction(MyAnimal:animal):void} = CatExampleДалее приведён ещё один пример, где переменная t используется только ковариантно.
# The line below will fail because t is used only covariantly.
MyFunction(:logic where t:type):?t = falseКонтрвариантность
Контрвариантность является антонимом ковариантности и означает использование чего-то более универсального, когда ожидается что-то более конкретное. Обычно это аргументы функции. В следующем примере универсальный параметрический тип payload используется контрвариантно.
DoSomething(Payload:payload where payload:type):voidПредположим, в приюте заведена особая процедура приёма новых кошек. Добавим в pet_sanctuary новый метод RegisterCat().
pet_sanctuary := class:
AdoptPet():animal = animal{}
RegisterCat(NewAnimal:cat):void = {}В cat_sanctuary нужно переопределить этот метод, чтобы он принимал animal в качестве параметра типа, поскольку мы уже знаем, что каждая кошка (cat) является животным (animal).
cat_sanctuary := class(pet_sanctuary):
AdoptPet<override>():cat = cat{}
RegisterCat<override>(NewAnimal:animal):void = {}В данном примере animal является контрвариантным по отношению к cat, поскольку мы используем что-то более универсальное, когда могло бы подойти что-то более конкретное.
Ковариантное использование неявного типа, введённого при помощи ключевого слова where, приведёт к ошибке. В следующем примере payload используется контрвариантно, однако возникает ошибка, так как он не определён в качестве аргумента.
DoSomething(:logic where payload:type) : ?payload = falseЧтобы это исправить, нужно переписать код, исключив параметр типа:
DoSomething(:logic) : ?false = falseИсключительно контрвариантное использование не будет приводить к ошибке, однако в этом случае код можно переписать, использовав any вместо false. Пример:
ReturnFirst(First:first_item, :second_item where first_item:type, second_item:type) : first_item = FirstПоскольку для параметра second_item был задан тип type и его значение не возвращается, во втором примере мы можем заменить его тип на any и не выполнять для него проверку типа.
ReturnFirst(First:first_item, :any where first_item:type) : first_item = FirstЗамена типа first_item на any или false сопровождается потерей точности. Например, следующий код не скомпилируется:
ReturnFirst(First:any, :any) :any = First
Main() : void =
FirstInt:int = ReturnFirst(1, "ignored")Известные ограничения
Параметры явного типа для типов данных могут использоваться только с классами, но не с интерфейсами или структурами. Наследование, связанное с параметрическими типами, также недопустимо. | Verse |
Параметрические типы могут рекурсивно ссылаться сами на себя (в случае прямой рекурсии). Параметрические типы не могут рекурсивно ссылаться на другие параметрические типы. | Verse |
На данный момент классы поддерживают только неизменяемые данные параметрического типа. К примеру, этот код не скомпилируется, поскольку | Verse |
Параметры явного типа можно без ограничений использовать с классом, а параметры неявного типа — с функцией. | Verse |