Tipos paramétricos se referem a qualquer tipo que pode receber um parâmetro. Você pode usar tipos paramétricos em Verse para definir estruturas e operações de dados generalizadas. Existem duas maneiras de usar tipos paramétricos como argumentos: em funções como argumentos de tipo explícito ou implícito ou em classes como argumentos de tipo explícito.
Eventos são um exemplo comum de tipos paramétricos usados extensivamente em todos os dispositivos no UEFN. Por exemplo, o dispositivo Botão tem InteractedWithEvent, que ocorre sempre que um jogador interage com o botão. Para ver um tipo paramétrico em ação, confira CountdownEndedEvent do tutorial Cronômetro de contagem regressiva personalizado.
Argumentos de tipo explícito
Considere uma box que recebe dois argumentos. O first_item inicializa um ItemOne, e o second_item inicializa um ItemTwo, ambos do tipo type. Tanto first_item quanto second_item são exemplos de tipos paramétricos que são argumentos explicit para uma classe.
box(first_item:type, second_item:type) := class:
ItemOne:first_item
ItemTwo:second_itemComo type é o argumento de tipo para first_item e second_item, a classe box pode ser criada com qualquer um dos dois tipos. Você pode ter uma caixa com dois valores string, uma caixa com dois valores int, uma string e um int, ou até mesmo uma caixa com duas caixas.
Para outro exemplo, considere a função MakeOption(), que usa qualquer tipo e retorna uma option desse tipo.
MakeOption(t:type):?t = false
IntOption := MakeOption(int)
FloatOption := MakeOption(float)
StringOption := MakeOption(string)Você pode modificar a função MakeOption() para, em vez disso, retornar qualquer outro tipo de contêiner, como um array ou um map.
Argumentos de tipo implícito
Argumentos de tipo implícito para funções são introduzidos usando a palavra-chave where. Por exemplo, considerando uma função ReturnItem(), que simplesmente usa um parâmetro e o retorna:
ReturnItem(Item:t where t:type):t = ItemAqui, t é um parâmetro de tipo implícito da função ReturnItem(), que recebe um argumento do tipo type e o retorna imediatamente. O tipo de t restringe que tipo de Item podemos passar para essa função. Nesse caso, como t é do tipo type, podemos chamar ReturnItem() com qualquer tipo. O motivo de usar tipos paramétricos implícitos com funções é que eles permitem que você escreva um código que funciona independentemente do tipo transmitido para ele.
Por exemplo, em vez de ter que escrever:
ReturnInt(Item:int):int = Item
ReturnFloat(Item:float):float = ItemA única função poderia ser escrita.
ReturnItem(Item:t where t:type):t = ItemCom isso, há a garantia de que ReturnItem() não precisa saber qual é o tipo específico de t: seja qual for a operação executada, ela funcionará independentemente do tipo de t.
O tipo real a ser usado para t depende de como ReturnItem() é usada. Por exemplo, se ReturnItem() for chamado com o argumento 0,0, t será um float.
ReturnItem("t") # t is a string
ReturnItem(0.0) # t is a floatNesse caso, "olá" e 0,0 são argumentos explícitos (o Item) passado para ReturnItem(). Ambos funcionarão porque o tipo implícito de Item é t, que pode ser qualquer type.
Para outro exemplo de um tipo paramétrico como um argumento implícito para uma função, considere a seguinte função MakeBox() que opera na 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")
Aqui, a função MakeBox() recebe dois argumentos, FirstItemVal e SecondItemVal, ambos do tipo type e retorna uma caixa do tipo (type, type). Usar type aqui significa que estamos dizendo a MakeBox que a caixa retornada pode ser composta de quaisquer dois objetos; pode ser um matriz, uma string, uma função etc. A função MakeBox() transmite ambos os argumentos para Box, usa-os para criar uma caixa e a retorna. Observe que tanto box quanto MakeBox() usam a mesma sintaxe como uma chamada de função.
Um exemplo integrado disso é a função para o tipo de contêiner Map, fornecido abaixo.
Map(F(:t) : u, X : []t) : []u =
for (Y : X):
F(Y)Restrições de tipo
Você pode especificar uma restrição no tipo de uma expressão. A única restrição atualmente aceita é o subtipo e somente para parâmetros de tipo implícito. Por exemplo:
int_box := class:
Item:int
MakeSubclassOfIntBox(NewBox:subtype_box where subtype_box:(subtype(int_box))) : tuple(subtype_box, int) = (NewBox, NewBox.Item)Neste exemplo, MakeSubclassOfIntBox() apenas será compilado quando passar uma classe que seja subclasse de IntBox, já que SubtypeBox tem o tipo (subtype(IntBox)). Note que type pode ser visto como um atalho para subtype(any). Em outras palavras, essa função aceita qualquer subtipo de any, que é todo tipo.
Covariância e contravariância
Covariância e contravariância se referem à relação de dois tipos quando esses tipos são usados em funções ou tipos compostos. Dois tipos relacionados de alguma forma, como quando um é a subclasse do outro, são covariantes ou contravariantes entre si, dependendo de como são usados em um determinado trecho de código.
Covariante: usar um tipo mais específico quando o código espera algo mais genérico.
Contravariante: usar um tipo mais geral quando o código espera algo mais específico.
Por exemplo, se pudéssemos usar um int em uma situação em que qualquer comparable seria aceito (como um float), nosso int estaria atuando como covariante, já que estamos usando um tipo mais específico quando se espera um mais genérico. Por outro lado, se pudéssemos usar qualquer comparable quando normalmente um int seria usado, nosso comparable estaria atuando como contravariante, já que estamos usando um tipo mais genérico quando se espera um mais específico.
Um exemplo de covariância e contravariância em um tipo paramétrico pode ser semelhante ao seguinte:
MyFunction(Input:t where t:type):logic = trueAqui, t é usado de forma contravariante como entrada para a função, e logic é usado como saída para a função.
É importante ter em mente que os dois tipos não são inerentemente covariantes ou contravariantes entre si. Sua atuação como covariantes ou contravariantes depende de como eles são usados no código.
Covariante
Covariância significa usar algo mais específico quando você espera algo genérico. Normalmente, isso é para a saída de uma função. Todos os usos de tipo que não são entradas para funções são usos covariantes.
Um exemplo de tipo paramétrico genérico abaixo tem payload atuando como covariante.
DoSomething():int =
payload:int = 0Por exemplo, suponha que tenhamos uma classe animal e uma classe cat que é uma subclasse de animal. Também temos uma classe pet_sanctuary que adota mascotes com a função AdoptPet(). Como não sabemos que tipo de mascote vamos obter, AdoptPet() retorna um animal genérico.
animal := class:
cat := class(animal):
pet_sanctuary := class:
AdoptPet():animal = animal{}Suponha que tenhamos outro santuário de mascotes que lide apenas com gatos. Essa classe, cat_sanctuary, é uma subclasse de pet_sanctuary. Como se trata de um santuário apenas para gatos, nós substituímos AdoptPet() para retornar apenas cat em vez de animal.
cat_sanctuary := class(pet_sanctuary):
AdoptPet<override>():cat = cat{}Neste caso, o tipo de retorno cat de AdoptPet() é covariante para animal. Estamos usando um tipo mais específico quando o original usou um mais geral.
Isso também pode se aplicar a tipos compostos. Dado uma matriz de cat, podemos inicializar uma matriz de animal usando a matriz cat. O oposto não funciona, pois animal não pode ser convertido em sua subclasse cat. A matriz cat é covariante da matriz animal, porque estamos tratando um tipo mais restrito como um tipo mais genérico.
CatArray:[]cat = array{}
AnimalArray:[]animal = CatArrayAs entradas para funções não podem ser usadas de maneira covariante. O código a seguir falhará porque a atribuição de AnimalExample(), para CatExample(), é do tipo cat, que é muito específico para ser o tipo de retorno de AnimalExample(). Inverter essa ordem atribuindo CatExample() a AnimalExample funcionaria devido à subtipagem de cat a partir de animal.
CatExample:type{CatFunction(MyCat:cat):void} = …
AnimalExample:type{AnimalFunction(MyAnimal:animal):void} = CatExampleSegue um exemplo adicional em que a variável t apenas é usada de maneira covariante.
# The line below will fail because t is used only covariantly.
MyFunction(:logic where t:type):?t = falseContravariante
Contravariância é o oposto de covariante e significa usar algo mais genérico quando você espera algo específico. Isso geralmente é inserido em uma função. Um exemplo de tipo paramétrico genérico abaixo tem payload atuando como contravariante.
DoSomething(Payload:payload where payload:type):voidDigamos que nosso santuário de mascotes tenha um procedimento específico para lidar com novos gatos. Adicionamos um novo método a pet_sanctuary chamado RegisterCat().
pet_sanctuary := class:
AdoptPet():animal = animal{}
RegisterCat(NewAnimal:cat):void = {}Para nosso cat_sanctuary, vamos sobrescrever esse método para aceitar um animal como um parâmetro de tipo porque já sabemos que todo cat é um animal.
cat_sanctuary := class(pet_sanctuary):
AdoptPet<override>():cat = cat{}
RegisterCat<override>(NewAnimal:animal):void = {}Aqui animal é contravariante de cat, já que estamos usando algo mais genérico quando algo mais específico funcionaria.
Usar um tipo implícito introduzido por uma cláusula where produz um erro de maneira covariante. Por exemplo, payload aqui é usado de forma contravariante, mas apresenta erros por não ser definido como um argumento.
DoSomething(:logic where payload:type) : ?payload = falsePara corrigir o erro, isso pode ser reescrito para excluir um parâmetro de tipo:
DoSomething(:logic) : ?false = falseUsos somente de contravariante não resultam em erro, mas podem ser reescritos usando any em vez de false. Por exemplo:
ReturnFirst(First:first_item, :second_item where first_item:type, second_item:type) : first_item = FirstComo second_item era do tipo type e não foi retornado, podemos substituí-lo por any no segundo exemplo e evitar a verificação de tipo.
ReturnFirst(First:first_item, :any where first_item:type) : first_item = FirstSubstituir o tipo first_item por any ou false faz com a precisão seja perdida. Por exemplo, o código a seguir falhará ao ser compilado:
ReturnFirst(First:any, :any) :any = First
Main() : void =
FirstInt:int = ReturnFirst(1, "ignored")Limitações conhecidas
Os tipos parâmetros explícitos para tipos de dados só podem ser usados com classes, não interfaces ou estruturas. A herança relacionada a tipos paramétricos também não é permitida. | Verse |
Tipos paramétricos podem fazer referência a si mesmos recursivamente desde que a recursão seja direta. Tipos paramétricos não podem referenciar recursivamente outros tipos paramétricos. | Verse |
Atualmente, as classes apenas oferecem suporte a dados de tipos paramétricos imutáveis. Por exemplo, esse código não seria compilado porque | Verse |
Os parâmetros de tipo explícito podem ser combinados livremente com uma classe, assim como os parâmetros de tipo implícito podem ser combinados com uma função. | Verse |