Eu acho que faz sentido explicar tipos existenciais junto com tipos universais, já que os dois conceitos são complementares, ou seja, um é o "oposto" do outro.
Não posso responder a todos os detalhes sobre tipos existenciais (como fornecer uma definição exata, listar todos os usos possíveis, sua relação com tipos abstratos de dados etc.) porque simplesmente não tenho conhecimento suficiente para isso. Vou demonstrar apenas (usando Java) o que este artigo do HaskellWiki afirma ser o principal efeito dos tipos existenciais:
Tipos existentes podem ser usados para vários propósitos diferentes. Mas o que eles fazem é "ocultar" uma variável de tipo no lado direito. Normalmente, qualquer variável de tipo que aparece à direita também deve aparecer à esquerda […]
Exemplo de configuração:
O pseudo-código a seguir não é Java totalmente válido, mesmo que fosse fácil o suficiente para corrigir isso. De fato, é exatamente isso que vou fazer nesta resposta!
class Tree<α>
{
α value;
Tree<α> left;
Tree<α> right;
}
int height(Tree<α> t)
{
return (t != null) ? 1 + max( height(t.left), height(t.right) )
: 0;
}
Deixe-me explicar brevemente isso para você. Estamos definindo ...
um tipo recursivo Tree<α>
que representa um nó em uma árvore binária. Cada nó armazena um value
de algum tipo α e possui referências a subárvores left
e opcionais right
do mesmo tipo.
uma função height
que retorna a maior distância de qualquer nó folha ao nó raiz t
.
Agora, vamos transformar o pseudocódigo acima height
na sintaxe Java apropriada! (Continuarei omitindo alguns clichês por questões de brevidade, como orientação a objetos e modificadores de acessibilidade.) Vou mostrar duas soluções possíveis.
1. Solução de tipo universal:
A correção mais óbvia é simplesmente tornar height
genérico introduzindo o parâmetro de tipo α em sua assinatura:
<α> int height(Tree<α> t)
{
return (t != null) ? 1 + max( height(t.left), height(t.right) )
: 0;
}
Isso permitiria declarar variáveis e criar expressões do tipo α dentro dessa função, se você quisesse. Mas...
2. Solução do tipo existencial:
Se você olhar para o corpo do nosso método, perceberá que não estamos realmente acessando ou trabalhando com algo do tipo α ! Não há expressões com esse tipo, nem nenhuma variável declarada com esse tipo ... então, por que precisamos tornar height
genéricos? Por que não podemos simplesmente esquecer α ? Como se vê, podemos:
int height(Tree<?> t)
{
return (t != null) ? 1 + max( height(t.left), height(t.right) )
: 0;
}
Como escrevi no começo desta resposta, os tipos existencial e universal são complementares / de natureza dupla. Assim, se a solução universal de tipos se tornasse height
mais genérica, devemos esperar que os tipos existenciais tenham o efeito oposto: torná-los menos genéricos, ou seja, ocultando / removendo o parâmetro de tipo α .
Como conseqüência, você não pode mais se referir ao tipo t.value
deste método nem manipular nenhuma expressão desse tipo, porque nenhum identificador foi vinculado a ele. (O ?
curinga é um token especial, não um identificador que "captura" um tipo.) t.value
Tornou-se efetivamente opaco; talvez a única coisa que você ainda possa fazer com ela seja a transmissão do tipo Object
.
Resumo:
===========================================================
| universally existentially
| quantified type quantified type
---------------------+-------------------------------------
calling method |
needs to know | yes no
the type argument |
---------------------+-------------------------------------
called method |
can use / refer to | yes no
the type argument |
=====================+=====================================