Leí el artículo de Wikipedia Tipos existenciales . Supuse que se llaman tipos existenciales debido al operador existencial (∃). Sin embargo, no estoy seguro de cuál es el punto. Cuál es la diferencia entre
T = ∃X { X a; int f(X); }
y
T = ∀x { X a; int f(X); }
?
Respuestas:
Cuando alguien define un tipo universal
∀X
, está diciendo: puede conectar el tipo que desee, no necesito saber nada sobre el tipo para hacer mi trabajo, solo me referiré a él de forma opacaX
.Cuando alguien define un tipo existencial
∃X
, está diciendo: usaré el tipo que quiera aquí; no sabrá nada sobre el tipo, por lo que solo puede referirse a él opacamente comoX
.Los tipos universales le permiten escribir cosas como:
La
copy
función no tiene idea de loT
que realmente será, pero no es necesario.Los tipos existenciales te permitirían escribir cosas como:
Cada implementación de máquina virtual en la lista puede tener un tipo de código de bytes diferente. La
runAllCompilers
función no tiene idea de cuál es el tipo de código de bytes, pero no es necesario; todo lo que hace es transmitir el bytecode deVirtualMachine.compile
aVirtualMachine.run
.Los comodines de tipo Java (ej .
List<?>
:) son una forma muy limitada de tipos existenciales.Actualización: Olvidé mencionar que puede simular tipos existenciales con tipos universales. Primero, ajuste su tipo universal para ocultar el parámetro de tipo. Segundo, invierta el control (esto efectivamente intercambia la parte "usted" y "yo" en las definiciones anteriores, que es la diferencia principal entre existenciales y universales).
Ahora podemos tener la
VMWrapper
llamada propiaVMHandler
que tiene unahandle
función de tipo universal . El efecto neto es el mismo, nuestro código debe tratarseB
como opaco.Un ejemplo de implementación de VM:
fuente
List<∃B:VirtualMachine<B>> vms
ofor (∃B:VirtualMachine<B> vm : vms)
. (Dado que estos son tipos genéricos, ¿no podría haber utilizado los?
comodines de Java en lugar de la sintaxis "propia"?) Creo que podría ser útil tener un ejemplo de código donde no haya tipos genéricos como los∃B:VirtualMachine<B>
involucrados, sino un "directo"∃B
, porque los tipos genéricos se asocian fácilmente con los tipos universales después de los primeros ejemplos de código.∃B
ser explícito sobre dónde está ocurriendo la cuantificación. Con la sintaxis comodín, el cuantificador está implícito (enList<List<?>>
realidad significa∃T:List<List<T>>
y noList<∃T:List<T>>
). Además, la cuantificación explícita le permite referirse al tipo (modifiqué el ejemplo para aprovechar esto almacenando el bytecode de tipoB
en una variable temporal).Un valor de un tipo existencial como
∃x. F(x)
es un par que contiene algún tipox
y un valor del tipoF(x)
. Mientras que un valor de tipo polimórfico como∀x. F(x)
es una función que toma algún tipox
y produce un valor de tipoF(x)
. En ambos casos, el tipo se cierra sobre algún tipo de constructorF
.Tenga en cuenta que esta vista combina tipos y valores. La prueba existencial es de un tipo y un valor. La prueba universal es una familia completa de valores indexados por tipo (o una asignación de tipos a valores).
Entonces, la diferencia entre los dos tipos que especificó es la siguiente:
Esto significa: Un valor de tipo
T
contiene un tipo llamadoX
, un valora:X
y una funciónf:X->int
. Un productor de valores de tipoT
puede elegir cualquier tipoX
y un consumidor no puede saber nada al respectoX
. Excepto que hay un ejemplo de que se llamaa
y que este valor se puede convertir en unint
dándolo af
. En otras palabras, un valor de tipoT
sabe producir deint
alguna manera. Bueno, podríamos eliminar el tipo intermedioX
y solo decir:El universalmente cuantificado es un poco diferente.
Esto significa: un valor de tipo
T
puede recibir cualquier tipoX
, y producirá un valora:X
y una funciónf:X->int
sin importar cuálX
sea . En otras palabras: un consumidor de valores de tipoT
puede elegir cualquier tipo deX
. Y un productor de valores de tipoT
no puede saber nada en absolutoX
, pero tiene que ser capaz de producir un valora
para cualquier elecciónX
y poder convertir dicho valor en unint
.Obviamente, implementar este tipo es imposible, porque no hay un programa que pueda producir un valor de cada tipo imaginable. A menos que permita absurdos como
null
o fondos.Como un existencial es un par, un argumento existencial puede convertirse en uno universal mediante currículum .
es lo mismo que:
El primero es un existencial de rango 2 . Esto lleva a la siguiente propiedad útil:
Existe un algoritmo estándar para convertir los existenciales en universales, llamado Skolemization .
fuente
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:
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!
Déjame explicarte esto brevemente. Estamos definiendo ...
Un tipo recursivo
Tree<α>
que representa un nodo en un árbol binario. Cada nodo almacena unvalue
tipo de α y tiene referencias a subtítulos opcionalesleft
yright
del mismo tipo.una función
height
que devuelve la distancia más lejana desde cualquier nodo hoja al nodo raízt
.¡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: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: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, set.value
ha vuelto opaco; tal vez lo único que aún puede hacer con él es moldearloObject
.Resumen:
fuente
Object
es bastante interesante: si bien ambos son similares, ya que le permiten escribir código estáticamente independiente del tipo, el primero (genéricos) no solo descarta casi toda la información de tipo disponible para alcanza esta meta. En este sentido particular, los genéricos son un remedio para laObject
OMI.public static void swap(List<?> list, int i, int j) { swapHelper(list, i, j); } private static <E> void swapHelper(List<E> list, int i, int j) { list.set(i, list.set(j, list.get(i))); }
,E
es ununiversal type
y?
representa unexistential type
??
in the typeint height(Tree<?> t)
aún no se conoce dentro de la función, y la persona que llama todavía lo determina porque es la persona que llamó quien eligió qué árbol pasar. Incluso si la gente llama a esto el tipo existencial en Java, no lo es. El?
marcador de posición se puede utilizar para implementar una forma de existenciales en Java, en algunas circunstancias, pero esta no es una de ellas.Todos estos son buenos ejemplos, pero elijo responderlo un poco diferente. Recordemos de las matemáticas, que ∀x. P (x) significa "para todas las x, puedo probar que P (x)". En otras palabras, es un tipo de función, me das una x y tengo un método para demostrártelo.
En teoría de tipos, no estamos hablando de pruebas, sino de tipos. Entonces en este espacio queremos decir "para cualquier tipo X que me des, te daré un tipo P específico". Ahora, dado que no le damos a P mucha información sobre X además del hecho de que es un tipo, P no puede hacer mucho con él, pero hay algunos ejemplos. P puede crear el tipo de "todos los pares del mismo tipo":
P<X> = Pair<X, X> = (X, X)
. O podemos crear el tipo de opción:,P<X> = Option<X> = X | Nil
donde Nil es el tipo de punteros nulos. Podemos hacer una lista fuera de él:List<X> = (X, List<X>) | Nil
. Observe que el último es recursivo, los valores deList<X>
son pares donde el primer elemento es una X y el segundo elemento es unList<X>
o es un puntero nulo.Ahora, en matemáticas ∃x. P (x) significa "Puedo probar que hay una x particular de tal manera que P (x) es verdadera". Puede haber muchas de esas x, pero para demostrarlo, una es suficiente. Otra forma de pensarlo es que debe existir un conjunto no vacío de pares de evidencia y prueba {(x, P (x))}.
Traducido a la teoría de tipos: Un tipo en la familia
∃X.P<X>
es un tipo X y un tipo correspondienteP<X>
. Tenga en cuenta que mientras antes le dimos X a P, (para que supiéramos todo sobre X pero P muy poco), lo contrario es cierto ahora.P<X>
no promete dar ninguna información sobre X, solo que hay una, y que de hecho es un tipo.¿Cómo es esto útil? Bueno, P podría ser un tipo que tiene una forma de exponer su tipo interno X. Un ejemplo sería un objeto que oculta la representación interna de su estado X. Aunque no tenemos forma de manipularlo directamente, podemos observar su efecto mediante hurgando en P. Podría haber muchas implementaciones de este tipo, pero podría usar todos estos tipos sin importar cuál se eligió en particular.
fuente
P<X>
lugar de unP
(misma funcionalidad y tipo de contenedor, digamos, pero no sabes que contieneX
)?∀x. P(x)
no significa nada acerca de la probabilidad deP(x)
, solo la verdad.Para responder directamente a su pregunta:
Con el tipo universal, los usos de
T
deben incluir el parámetro de tipoX
. Por ejemploT<String>
oT<Integer>
. Para los usos de tipo existenciales deT
no incluya ese parámetro de tipo porque es desconocido o irrelevante, solo useT
(o en JavaT<?>
).Más información:
Los tipos universales / abstractos y los tipos existenciales son una dualidad de perspectiva entre el consumidor / cliente de un objeto / función y el productor / implementación del mismo. Cuando un lado ve un tipo universal, el otro ve un tipo existencial.
En Java puedes definir una clase genérica:
MyClass
,T
es universal porque se puede sustituir cualquier tipo paraT
cuando se utiliza esa clase y se debe conocer el tipo real de T cada vez que utilice una instancia deMyClass
MyClass
sí mismo,T
es existencial porque no conoce el tipo real deT
?
representa el tipo existencial, por lo tanto, cuando estás dentro de la clase,T
es básicamente?
. Si desea manejar una instancia deMyClass
conT
existencial, puede declararMyClass<?>
como en elsecretMessage()
ejemplo anterior.Los tipos existenciales a veces se usan para ocultar los detalles de implementación de algo, como se discutió en otra parte. Una versión Java de esto podría verse así:
Es un poco complicado capturar esto correctamente porque pretendo estar en algún tipo de lenguaje de programación funcional, que Java no es. Pero el punto aquí es que está capturando algún tipo de estado más una lista de funciones que operan en ese estado y no conoce el tipo real de la parte del estado, pero las funciones sí lo saben ya que ya estaban emparejadas con ese tipo .
Ahora, en Java, todos los tipos no finales no primitivos son parcialmente existenciales. Esto puede sonar extraño, pero debido a que una variable declarada como
Object
potencialmente podría ser una subclaseObject
, no puede declarar el tipo específico, solo "este tipo o una subclase". Por lo tanto, los objetos se representan como un bit de estado más una lista de funciones que operan en ese estado: exactamente qué función llamar se determina en tiempo de ejecución por búsqueda. Esto es muy parecido al uso de los tipos existenciales anteriores donde tiene una parte de estado existencial y una función que opera en ese estado.En lenguajes de programación con tipos estáticos sin subtipos y conversiones, los tipos existenciales le permiten a uno administrar listas de objetos con tipos diferentes. Una lista de
T<Int>
no puede contener aT<Long>
. Sin embargo, una lista deT<?>
puede contener cualquier variación deT
, lo que permite colocar muchos tipos diferentes de datos en la lista y convertirlos a un int (o hacer cualquier operación que se proporcione dentro de la estructura de datos) a pedido.Casi siempre se puede convertir un registro con un tipo existencial en un registro sin usar cierres. Un cierre también se escribe existencialmente, ya que las variables libres sobre las que se cierra están ocultas para la persona que llama. Por lo tanto, un lenguaje que admite cierres pero no tipos existenciales puede permitirle hacer cierres que compartan el mismo estado oculto que habría puesto en la parte existencial de un objeto.
fuente
Un tipo existencial es un tipo opaco.
Piense en un identificador de archivo en Unix. Sabes que su tipo es int, por lo que puedes falsificarlo fácilmente. Puede, por ejemplo, intentar leer desde el identificador 43. Si sucede que el programa tiene un archivo abierto con este identificador en particular, lo leerá. Su código no tiene que ser malicioso, solo descuidado (por ejemplo, el identificador podría ser una variable no inicializada).
Un tipo existencial está oculto de su programa. Si se
fopen
devuelve un tipo existencial, todo lo que podría hacer con él es usarlo con algunas funciones de biblioteca que acepten este tipo existencial. Por ejemplo, se compilaría el siguiente pseudocódigo:La interfaz "leer" se declara como:
Existe un tipo T tal que:
La variable exfile no es un
char*
archivo int, ni un archivo struct, nada que pueda expresar en el sistema de tipos. No puede declarar una variable cuyo tipo es desconocido y no puede lanzar, por ejemplo, un puntero a ese tipo desconocido. El idioma no te deja.fuente
read
es,∃T.read(T file, ...)
entonces no hay nada que pueda pasar como primer parámetro. Lo que funcionaría esfopen
devolver el identificador de archivo y una función de lectura con el mismo alcance existencial :∃T.(T, read(T file, ...))
Parece que llego un poco tarde, pero de todos modos, este documento agrega otra visión de lo que son los tipos existenciales, aunque no es específicamente agnóstico al lenguaje, debería ser bastante más fácil de entender los tipos existenciales: http: //www.cs.uu .nl / groups / ST / Projects / ehc / ehc-book.pdf (capítulo 8)
fuente
Existe un tipo universal para todos los valores de los parámetros de tipo. Un tipo existencial existe solo para valores de los parámetros de tipo que satisfacen las restricciones del tipo existencial.
Por ejemplo, en Scala, una forma de expresar un tipo existencial es un tipo abstracto que está limitado a algunos límites superiores o inferiores.
De manera equivalente, un tipo universal restringido es un tipo existencial como en el siguiente ejemplo.
Cualquier sitio de uso puede emplear el
Interface
porque cualquier subtipo de instancia deExistential
debe definir eltype Parameter
que debe implementar elInterface
.Un caso degenerado de un tipo existencial en Scala es un tipo abstracto al que nunca se hace referencia y, por lo tanto, no necesita ser definido por ningún subtipo. Esto efectivamente tiene una notación abreviada de
List[_]
en Scala yList<?>
en Java.Mi respuesta se inspiró en la propuesta de Martin Odersky de unificar tipos abstractos y existenciales. La diapositiva adjunta ayuda a la comprensión.
fuente
∀x.f(x)
son opacos para sus funciones receptoras, mientras que los tipos existenciales∃x.f(x)
están limitados a tener ciertas propiedades. Típicamente, todos los parámetros son Existenciales ya que su función los manipulará directamente; sin embargo, los parámetros genéricos pueden tener tipos que son universales, ya que la función no los administrará más allá de las operaciones universales básicas, como obtener una referencia como en:∀x.∃array.copy(src:array[x] dst:array[x]){...}
forSome
para el parámetro tipo cuantificación existencial.La investigación de tipos de datos abstractos y el ocultamiento de información trajeron tipos existenciales a lenguajes de programación. Hacer un resumen de tipo de datos oculta información sobre ese tipo, por lo que un cliente de ese tipo no puede abusar de él. Digamos que tiene una referencia a un objeto ... algunos lenguajes le permiten convertir esa referencia a una referencia a bytes y hacer lo que quiera con ese trozo de memoria. Con el fin de garantizar el comportamiento de un programa, es útil que un lenguaje imponga que solo actúes sobre la referencia al objeto a través de los métodos que proporciona el diseñador del objeto. Sabes que el tipo existe, pero nada más.
fuente
Creé este diagrama. No sé si es riguroso. Pero si ayuda, me alegro.
fuente
Según tengo entendido, es una forma matemática de describir interfaces / clase abstracta.
En cuanto a T = ∃X {X a; int f (X); }
Para C # se traduciría a un tipo abstracto genérico:
"Existencial" solo significa que hay algún tipo que obedece a las reglas definidas aquí.
fuente