Los tipos paramétricos refieren a cualquier tipo que pueda tomar un parámetro. Puedes utilizar tipos paramétricos en Verse para definir estructuras de datos y operaciones generalizadas. Existen dos formas de usar 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 usan mucho en los dispositivos de UEFN. Por ejemplo, el dispositivo de botón tiene InteractedWithEvent, que ocurre cada vez que un jugador interactúa con el botón. Para ver un tipo paramétrico en acción, consulta el CountdownEndedEvent del tutorial del cronómetro personalizado de cuenta regresiva.
Argumentos de tipo explícito
Imaginemos una box que toma dos argumentos. El first_item inicializa un ItemOne, y el second_item inicializa un ItemTwo, ambos de tipo type. Tanto first_item como second_item son ejemplos de tipos paramétricos que son argumentos explícitos de una clase.
box(first_item:type, second_item:type) := class:
ItemOne:first_item
ItemTwo:second_itemDado que type es el argumento de tipo para first_item y second_item, la clase box puede crearse con dos tipos cualesquiera. Puedes tener un cuadro con dos valores string, un cuadro con dos valores int, una string y un int, ¡o incluso un cuadro con dos cuadros!
Otro ejemplo es la función MakeOption(), que toma cualquier tipo y devuelve una opción de ese 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 una array o un map.
Argumentos de tipo implícito
Los argumentos de tipo implícito para las funciones se introducen con 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 para usar tipos paramétricos implícitos con funciones es que permite escribir código que funciona independientemente del tipo que se le pase.
Por ejemplo, en lugar del siguiente código:
ReturnInt(Item:int):int = Item
ReturnFloat(Item:float):float = ItemSe podría escribir la función única en su lugar.
ReturnItem(Item:t where t:type):t = ItemEsto garantiza que ReturnItem() no necesite saber 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 los argumentos explícitos (el Item) que se pasa a ReturnItem(). Ambos funcionarán porque el tipo implícito de Item es t, que puede ser cualquier type.
Otro ejemplo de un tipo paramétrico como argumento implícito de una función es 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 un cuadro de tipo (type, type). Al usar type aquí, le estamos diciendo a MakeBox que el cuadro devuelto puede estar formado por dos objetos cualesquiera; puede ser una matriz, una cadena, una función, etc. La función MakeBox() pasa ambos argumentos a Box y los utiliza para crear un cuadro y lo devuelve. Ten en cuenta que tanto box como MakeBox() usan la misma sintaxis que una llamada a la función.
Un ejemplo integrado de esto es la función para el tipo de contenedor Map, que se ofrece a continuación.
Map(F(:t) : u, X : []t) : []u =
for (Y : X):
F(Y)Restricciones de tipo
Puedes especificar una restricción en el tipo de una expresión. La única restricción actualmente compatible 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á cuando se le pase una clase que sea subclase de IntBox, ya que SubtypeBox tiene el tipo (subtype(IntBox)). Ten en cuenta que type puede verse como una abreviatura de subtype(any). En otras palabras, esta función acepta cualquier subtipo de any, que es cualquier tipo.
Covariancia y contravariancia
La covariancia y la contravariancia 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 alguna manera, como cuando uno es subclase del otro, son covariantes o contravariantes entre sí en función de cómo se utilicen en un determinado fragmento de código.
Covariante: utilizar un tipo más específico cuando el código espera algo más genérico.
Contravariante: usar un tipo más genérico cuando el código espera algo más específico.
Por ejemplo, si pudiéramos usar un int en una situación en la que se aceptaría cualquier comparable (como un float), nuestro int estaría actuando covariantemente, ya que estamos usando un tipo más específico cuando se espera uno más genérico. A la inversa, si pudiéramos usar cualquier comparable cuando normalmente se usaría un int, nuestro comparable estaría actuando contravariantemente, ya que estamos usando un tipo más genérico cuando se espera uno más específico.
Un ejemplo de covariancia y contravariancia en un tipo paramétrico podría ser el siguiente:
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
La covariancia consiste en 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.
El siguiente ejemplo de tipo paramétrico genérico tiene payload que actúa de forma covariante.
DoSomething():int =
payload:int = 0Por ejemplo, supongamos que tenemos una clase animal y una clase cat que es una 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 tener, AdoptPet() devuelve un animal genérico.
animal := class:
cat := class(animal):
pet_sanctuary := class:
AdoptPet():animal = animal{}Supongamos que tenemos otro santuario de mascotas que solo trabaja con gatos. Esta clase, cat_sanctuary, es una subclase de pet_sanctuary. Como este es un santuario de gatos, anulamos AdoptPet() para que solo devuelva un gato en lugar de un 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 cat. A la inversa no funciona, ya que animal no puede convertirse en su subclase cat. La matriz de cat es covariante con la matriz de animal, porque estamos tratando un tipo más específico como un tipo más genérico.
CatArray:[]cat = array{}
AnimalArray:[]animal = CatArrayLas entradas de las funciones no pueden utilizarse de forma covariante. El siguiente código fallará porque la asignación de AnimalExample() a CatExample() es de tipo cat, que es demasiado específico para ser el tipo de devolución de AnimalExample(). Si se invierte este orden asignando CatExample() a AnimalExample funcionará debido a que 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 contravariancia es lo contrario de la covariancia, y consiste en utilizar algo más genérico cuando se espera algo específico. Suele ser la entrada de una función. El siguiente ejemplo de tipo paramétrico genérico tiene payload que actúa de forma contravariante.
DoSomething(Payload:payload where payload:type):voidDigamos que nuestro santuario de mascotas tiene un procedimiento específico para tratar a los gatos nuevos. Añadimos un nuevo método a pet_sanctuary llamado RegisterCat().
pet_sanctuary := class:
AdoptPet():animal = animal{}
RegisterCat(NewAnimal:cat):void = {}Para nuestro cat_sanctuary, vamos a anular este método de modo 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 = {}Acá animal es contravariante de cat, ya que estamos usando algo más genérico cuando algo más específico funcionaría.
El uso de un tipo implícito introducido por una cláusula where covariante produce un error. Por ejemplo, payload se utiliza, en este caso, de forma contravariante, pero da error al no estar definido como argumento.
DoSomething(:logic where payload:type) : ?payload = falseA fin de solucionarlo, podría reescribirse para excluir un parámetro de tipo:
DoSomething(:logic) : ?false = falseLos usos solo contravariantes no producen errores, 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 una comprobación de tipo sobre él.
ReturnFirst(First:first_item, :any where first_item:type) : first_item = FirstSi se reemplaza el tipo first_item por any o false se pierde precisión. Por ejemplo, la compilación del código siguiente fallará:
ReturnFirst(First:any, :any) :any = First
Main() : void =
FirstInt:int = ReturnFirst(1, "ignored")Limitaciones conocidas
Los parámetros de tipo explícito para los tipos de datos solo pueden usarse con clases, no con interfaces ni estructuras. La herencia en relación 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, del mismo modo que los parámetros de tipo implícito pueden combinarse con una función. | Verse |