Creo que tiene sentido explicar los tipos existenciales junto con los tipos universales, ya que los dos conceptos son complementarios, es decir, uno es el "opuesto" del otro.
No puedo responder a cada detalle sobre los tipos existenciales (como dar una definición exacta, enumerar todos los usos posibles, su relación con los tipos de datos abstractos, etc.) porque simplemente no tengo el conocimiento suficiente para eso. Solo demostraré (usando Java) lo que este artículo de HaskellWiki dice que es el efecto principal de los tipos existenciales:
Los tipos existenciales se pueden usar para varios propósitos diferentes. Pero lo que hacen es 'ocultar' una variable de tipo en el lado derecho. Normalmente, cualquier variable de tipo que aparezca a la derecha también debe aparecer a la izquierda […]
Ejemplo de configuración:
El siguiente pseudocódigo no es Java válido, a pesar de que sería lo suficientemente fácil de solucionar. De hecho, ¡eso es exactamente lo que voy a hacer en esta respuesta!
class Tree<α>
{
α value;
Tree<α> left;
Tree<α> right;
}
int height(Tree<α> t)
{
return (t != null) ? 1 + max( height(t.left), height(t.right) )
: 0;
}
Déjame explicarte esto brevemente. Estamos definiendo ...
Un tipo recursivo Tree<α>
que representa un nodo en un árbol binario. Cada nodo almacena un value
tipo de α y tiene referencias a subtítulos opcionales left
y right
del mismo tipo.
una función height
que devuelve la distancia más lejana desde cualquier nodo hoja al nodo raíz t
.
¡Ahora, cambiemos el pseudocódigo anterior height
a la sintaxis Java correcta! (Seguiré omitiendo algunas repeticiones por razones de brevedad, como la orientación a objetos y los modificadores de accesibilidad). Voy a mostrar dos posibles soluciones.
1. Solución de tipo universal:
La solución más obvia es simplemente hacer height
genérico introduciendo el parámetro de tipo α en su firma:
<α> int height(Tree<α> t)
{
return (t != null) ? 1 + max( height(t.left), height(t.right) )
: 0;
}
Esto le permitiría declarar variables y crear expresiones de tipo α dentro de esa función, si así lo desea. Pero...
2. Solución de tipo existencial:
Si observa el cuerpo de nuestro método, notará que en realidad no estamos accediendo o trabajando con nada del tipo α . No hay expresiones que tengan ese tipo, ni ninguna variable declarada con ese tipo ... entonces, ¿por qué tenemos que hacer algo height
genérico? ¿Por qué no podemos simplemente olvidarnos de α ? Como resultado, podemos:
int height(Tree<?> t)
{
return (t != null) ? 1 + max( height(t.left), height(t.right) )
: 0;
}
Como escribí al comienzo de esta respuesta, los tipos existenciales y universales son de naturaleza complementaria / dual. Por lo tanto, si la solución de tipo universal fuera hacer height
más genérico, entonces deberíamos esperar que los tipos existenciales tengan el efecto contrario: hacerlo menos genérico, es decir, al ocultar / eliminar el parámetro de tipo α .
Como consecuencia, ya no puede hacer referencia al tipo de t.value
en este método ni manipular ninguna expresión de ese tipo, porque no se ha vinculado ningún identificador. (El ?
comodín es un token especial, no un identificador que "captura" un tipo). De hecho, se t.value
ha vuelto opaco; tal vez lo único que aún puede hacer con él es moldearlo Object
.
Resumen:
===========================================================
| 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 |
=====================+=====================================