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 valuede algum tipo α e possui referências a subárvores lefte opcionais rightdo mesmo tipo.
uma função heightque retorna a maior distância de qualquer nó folha ao nó raiz t.
Agora, vamos transformar o pseudocódigo acima heightna 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 heightgené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 heightgené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.valuedeste 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.valueTornou-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 |
=====================+=====================================