Los tipos paramétricos hacen referencia a cualquier tipo que pueda aceptar un parámetro. Puedes utilizar tipos paramétricos en Verse para definir estructuras de datos y operaciones generalizadas. Hay dos formas de utilizar tipos paramétricos como argumentos: en funciones como argumentos de tipo explícito o implícito, o en clases como argumentos de tipo explícito.
Los eventos son un ejemplo común de tipos paramétricos y se utilizan mucho en todos los dispositivos de UEFN. Por ejemplo, el dispositivo Botón tiene InteractedWithEvent, que se produce siempre que un jugador interactúa con el botón. Para ver un tipo paramétrico en acción, echa un vistazo al CountdownEndedEvent del Tutorial de cronómetro personalizado.
Argumentos de tipo explícito
Considera una clase box que recibe dos argumentos. first_item inicializa ItemOne, y second_item inicializa ItemTwo, ambos de tipo type. Tanto first_item como second_item son ejemplos de tipos paramétricos que son argumentos explícitos para una clase.
box(first_item:type, second_item:type) := class:
ItemOne:first_item
ItemTwo:second_itemComo type es el argumento de tipo para first_item y second_item`, la clase box puede crearse con dos tipos cualesquiera. Puedes tener una caja con dos valores string, una caja con dos valores int, un string y un int, ¡o incluso una caja con dos cajas!
Para otro ejemplo, considera la función MakeOption(), que toma cualquier tipo y devuelve un option de dicho tipo.
MakeOption(t:type):?t = false
IntOption := MakeOption(int)
FloatOption := MakeOption(float)
StringOption := MakeOption(string)Puedes modificar la función MakeOption() para que devuelva cualquier otro tipo de contenedor, como un array o un map.
Argumentos de tipo implícito
Los argumentos de tipo implícito para las funciones se introducen mediante la palabra clave where. Por ejemplo, dada una función ReturnItem(), que simplemente toma un parámetro y lo devuelve:
ReturnItem(Item:t where t:type):t = ItemAquí, t es un parámetro de tipo implícito de la función ReturnItem(), que toma un argumento de tipo type y lo devuelve inmediatamente. El tipo de t restringe qué tipo de Item podemos pasar a esta función. En este caso, como t es de tipo type, podemos llamar a ReturnItem() con cualquier tipo. La razón de utilizar tipos paramétricos implícitos con las funciones es que te permite escribir código que funcione independientemente del tipo que se le pase.
Por ejemplo, en lugar de tener que escribir:
ReturnInt(Item:int):int = Item
ReturnFloat(Item:float):float = ItemEn su lugar se podría escribir la función única.
ReturnItem(Item:t where t:type):t = ItemEsto garantiza que ReturnItem() no necesita saber de qué tipo concreto es t: cualquier operación que realice, funcionará independientemente del tipo de t.
El tipo real que debe utilizarse para t depende de cómo se utilice ReturnItem(). Por ejemplo, si se llama a ReturnItem() con el argumento 0.0, entonces t es un float.
ReturnItem("t") # t is a string
ReturnItem(0.0) # t is a floatAquí "hello" y 0.0 son el argumento explícito (el Item) pasado a ReturnItem(). Ambas funcionarán porque el tipo implícito de Item es t, que puede ser cualquier type.
Para ver otro ejemplo de un tipo paramétrico como argumento implícito de una función, considera la siguiente función MakeBox() que opera sobre la clase 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")
Aquí la función MakeBox() toma dos argumentos, FirstItemVal y SecondItemVal, ambos de tipo type, y devuelve una caja de tipo (type, type). Utilizar type significa que le estamos diciendo a MakeBox que la caja devuelta puede estar formada por dos objetos cualesquiera; puede ser una matriz, una cadena, una función, etc. La función MakeBox() pasa ambos argumentos a Box, los utiliza para crear una caja y la devuelve. Ten en cuenta que tanto box como MakeBox() utilizan la misma sintaxis que una llamada a la función.
Un ejemplo incorporado de esto es la función para el tipo de contenedor Map, a continuación.
Map(F(:t) : u, X : []t) : []u =
for (Y : X):
F(Y)Restricciones de tipo
Puedes especificar una restricción sobre el tipo de una expresión. La única restricción admitida actualmente es el subtipo, y solo para parámetros de tipo implícito. Por ejemplo:
int_box := class:
Item:int
MakeSubclassOfIntBox(NewBox:subtype_box where subtype_box:(subtype(int_box))) : tuple(subtype_box, int) = (NewBox, NewBox.Item)En este ejemplo, MakeSubclassOfIntBox() solo compilará si se le pasa una clase que sea subclase de IntBox, ya que `SubtypeBox` tiene el tipo (subtype(IntBox)). Ten en cuenta que type puede considerarse una abreviatura de subtype(any). Es decir, esta función acepta cualquier subtipo de any, que es cualquier tipo.
Covarianza y contravarianza
La covarianza y la contravarianza se refieren a la relación de dos tipos cuando los tipos se utilizan en tipos compuestos o funciones. Dos tipos que están relacionados de algún modo, como cuando uno es una subclase del otro, son covariantes o contravariantes entre sí, dependiendo de cómo se utilicen en un determinado fragmento de código.
Covariante: utiliza un tipo más específico cuando el código espera algo más genérico.
Contravariante: utiliza un tipo más general cuando el código espera algo más específico.
Por ejemplo, si pudiéramos utilizar un int en una situación en la que se aceptaría cualquier comparable (como un float), nuestro int estaría actuando de forma covariante, ya que estamos utilizando un tipo más específico cuando se espera uno más genérico. A la inversa, si pudiéramos utilizar cualquier comparable cuando normalmente se utilizaría un int, nuestro comparable estaría actuando de forma contravariante, ya que estamos utilizando un tipo más genérico cuando se espera uno más específico.
Un ejemplo de covarianza y contravarianza en un tipo paramétrico podría tener el siguiente aspecto:
MyFunction(Input:t where t:type):logic = trueAquí t se utiliza de forma contravariante como entrada de la función, y logic se utiliza de forma covariante como salida de la función.
Es importante tener en cuenta que los dos tipos no son inherentemente covariantes o contravariantes entre sí, sino que el hecho de que actúen como covariantes o contravariantes depende de cómo se utilicen en el código.
Covariante
Covariar significa utilizar algo más específico cuando se espera algo genérico. Normalmente se trata de la salida de una función. Todos los usos de tipos que no son entradas a funciones son usos covariantes.
Un ejemplo de tipo paramétrico genérico a continuación tiene payload actuando de forma covariante.
DoSomething():int =
payload:int = 0Por ejemplo, supongamos que tenemos una clase animal y una clase cat que sea subclase de animal. También tenemos una clase pet_sanctuary que adopta mascotas con la función AdoptPet(). Como no sabemos qué tipo de mascota vamos a conseguir, AdoptPet() devuelve un animal genérico.
animal := class:
cat := class(animal):
pet_sanctuary := class:
AdoptPet():animal = animal{}Supongamos que tenemos otro refugio de mascotas que solo se ocupa de gatos. Esta clase, cat_sanctuary, es una subclase de pet_sanctuary. Como se trata de un refugio para gatos, anulamos AdoptPet() para devolver solo cat en vez de animal.
cat_sanctuary := class(pet_sanctuary):
AdoptPet<override>():cat = cat{}En este caso, el tipo de retorno cat de AdoptPet() es covariante a animal. Estamos utilizando un tipo más específico cuando el original utilizaba uno más general.
Esto también puede aplicarse a los tipos compuestos. Dada una matriz de cat, podemos inicializar una matriz de animal utilizando la matriz de gatos. Lo contrario no funciona, ya que animal no puede convertirse en su subclase cat. La matriz de cat es covariante de la matriz de animal, porque estamos tratando un tipo más estrecho como un tipo más genérico.
CatArray:[]cat = array{}
AnimalArray:[]animal = CatArrayLas entradas de las funciones no pueden utilizarse de forma covariante. El código siguiente fallará porque la asignación de AnimalExample(), a CatExample(), es del tipo cat, que es demasiado específico para ser el tipo de retorno de AnimalExample(). Invertir este orden asignando CatExample() a AnimalExample funcionaría porque cat es subtipo de animal.
CatExample:type{CatFunction(MyCat:cat):void} = …
AnimalExample:type{AnimalFunction(MyAnimal:animal):void} = CatExampleA continuación se presenta un ejemplo adicional en el que la variable t solo se utiliza de forma covariante.
# The line below will fail because t is used only covariantly.
MyFunction(:logic where t:type):?t = falseContravariante
La contravarianza es lo contrario de la covarianza, y significa utilizar algo más genérico cuando se espera algo específico. Suele ser la entrada de una función. Un ejemplo de tipo paramétrico genérico a continuación tiene payload actuando de forma contravariante.
DoSomething(Payload:payload where payload:type):voidDigamos que nuestro refugio de mascotas tiene un procedimiento de ingreso específico para los gatos nuevos. Añadimos un nuevo método a pet_sanctuary nombrado RegisterCat().
pet_sanctuary := class:
AdoptPet():animal = animal{}
RegisterCat(NewAnimal:cat):void = {}Para nuestro cat_sanctuary, vamos a anular este método para que acepte un animal como parámetro de tipo, porque ya sabemos que todo cat es un animal.
cat_sanctuary := class(pet_sanctuary):
AdoptPet<override>():cat = cat{}
RegisterCat<override>(NewAnimal:animal):void = {}Aquí animal es contravariante de cat, ya que estamos utilizando algo más genérico cuando funcionaría algo más específico.
Utilizar un tipo implícito introducido por una cláusula where covariante produce un error. Por ejemplo, aquí payload se utiliza de forma contravariante, pero da error por no estar definido como argumento.
DoSomething(:logic where payload:type) : ?payload = falsePara solucionarlo, se podría reescribir para excluir un parámetro de tipo:
DoSomething(:logic) : ?false = falseLos usos solo contravariantes no provocan un error, pero pueden reescribirse utilizando any en lugar de false. Por ejemplo:
ReturnFirst(First:first_item, :second_item where first_item:type, second_item:type) : first_item = FirstComo second_item era de tipo type y no se devolvió, podemos sustituirlo por any en el segundo ejemplo y evitar hacer comprobaciones de tipo sobre él.
ReturnFirst(First:first_item, :any where first_item:type) : first_item = FirstAl sustituir el tipo first_item por any o false se pierde precisión. Por ejemplo, el código siguiente no se compilará:
ReturnFirst(First:any, :any) :any = First
Main() : void =
FirstInt:int = ReturnFirst(1, "ignored")Limitaciones conocidas
Los parámetros de tipo explícito con respecto a los tipos de datos solo pueden utilizarse con clases, y no con interfaces o estructuras. La herencia relacionada con los tipos paramétricos tampoco está permitida. | Verse |
Los tipos paramétricos pueden referenciarse a sí mismos recursivamente siempre que la recursión sea directa. Los tipos paramétricos no pueden referenciar recursivamente a otros tipos paramétricos. | Verse |
Actualmente, las clases solo admiten datos de tipo paramétrico inmutables. Por ejemplo, este código no compilaría porque | Verse |
Los parámetros de tipo explícito pueden combinarse libremente con una clase, solo como los parámetros de tipo implícito pueden combinarse con una función. | Verse |