Eu li o artigo da Wikipedia Tipos existenciais . Concluí que eles são chamados de tipos existenciais por causa do operador existencial (∃). Não tenho certeza de qual é o objetivo disso. Qual é a diferença entre
T = ∃X { X a; int f(X); }
e
T = ∀x { X a; int f(X); }
?
Respostas:
Quando alguém define um tipo universal,
∀X
eles estão dizendo: Você pode conectar o tipo que quiser, não preciso saber nada sobre o tipo para fazer meu trabalho, apenas me referirei a ele de forma opostaX
.Quando alguém define um tipo existencial,
∃X
eles estão dizendo: usarei o tipo que quiser aqui; você não saberá nada sobre o tipo; portanto, você só pode se referir a ele de forma oposta comoX
.Os tipos universais permitem escrever coisas como:
A
copy
função não tem idéia do queT
realmente será, mas não precisa.Os tipos existentes permitem escrever coisas como:
Cada implementação de máquina virtual na lista pode ter um tipo de bytecode diferente. A
runAllCompilers
função não tem idéia de qual é o tipo de bytecode, mas não precisa; tudo o que faz é retransmitir o bytecode deVirtualMachine.compile
paraVirtualMachine.run
.Os curingas do tipo Java (ex:)
List<?>
são uma forma muito limitada de tipos existenciais.Atualização: Esqueci de mencionar que você pode simular tipos existenciais com tipos universais. Primeiro, envolva seu tipo universal para ocultar o parâmetro type. Segundo, controle invertido (isso efetivamente troca a parte "você" e "I" nas definições acima, que é a principal diferença entre existenciais e universais).
Agora podemos ter
VMWrapper
nossa própria chamada,VMHandler
que possui umahandle
função de tipo universal . O efeito líquido é o mesmo, nosso código deve ser tratadoB
como opaco.Um exemplo de implementação de VM:
fonte
List<∃B:VirtualMachine<B>> vms
orfor (∃B:VirtualMachine<B> vm : vms)
. (Como esses são tipos genéricos, você não poderia ter usado os?
curingas de Java em vez da sintaxe "criada por você"?). Acho que pode ser útil ter um exemplo de código em que nenhum tipo genérico∃B:VirtualMachine<B>
esteja envolvido, mas sim um "direto"∃B
, porque tipos genéricos são facilmente associados a tipos universais após seus primeiros exemplos de código.∃B
ser explícito sobre onde a quantificação está acontecendo. Com a sintaxe curinga, o quantificador está implícito (List<List<?>>
na verdade significa∃T:List<List<T>>
e nãoList<∃T:List<T>>
). Além disso, a quantificação explícita permite que você se refira ao tipo (modifiquei o exemplo para tirar vantagem disso armazenando o bytecode do tipoB
em uma variável temporária).Um valor de um tipo existencial como
∃x. F(x)
é um par que contém algum tipox
e um valor do tipoF(x)
. Considerando que um valor de um tipo polimórfico como∀x. F(x)
é uma função que pega algum tipox
e produz um valor de tipoF(x)
. Nos dois casos, o tipo fecha sobre algum construtor de tipoF
.Observe que essa visão combina tipos e valores. A prova existencial é um tipo e um valor. A prova universal é uma família inteira de valores indexados por tipo (ou um mapeamento de tipos para valores).
Portanto, a diferença entre os dois tipos que você especificou é a seguinte:
Isso significa: Um valor do tipo
T
contém um tipo chamadoX
, um valora:X
e uma funçãof:X->int
. Um produtor de valores do tipoT
pode escolher qualquer tipoX
e o consumidor não pode saber nadaX
. Exceto que há um exemplo disso chamadoa
e que esse valor pode ser transformado emint
, dando-o af
. Em outras palavras, um valor do tipoT
sabe como produzir umaint
maneira. Bem, podemos eliminar o tipo intermediárioX
e apenas dizer:O quantificado universalmente é um pouco diferente.
Isso significa: Um valor do tipo
T
pode receber qualquer tipoX
e produzirá um valora:X
e uma função,f:X->int
independentemente de qualX
seja . Em outras palavras: um consumidor de valores do tipoT
pode escolher qualquer tipo paraX
. E um produtor de valores do tipoT
não pode saber absolutamente nadaX
, mas deve ser capaz de produzir um valora
para qualquer escolhaX
e transformar esse valor em umint
.Obviamente, implementar esse tipo é impossível, porque não existe um programa que possa produzir um valor para todos os tipos imagináveis. A menos que você permita absurdos como
null
ou fundos.Como um existencial é um par, um argumento existencial pode ser convertido em universal por meio de currying .
é o mesmo que:
O primeiro é um existencial de classificação 2 . Isso leva à seguinte propriedade útil:
Existe um algoritmo padrão para transformar existenciais em universais, chamado Skolemization .
fonte
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:
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!
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 umvalue
de algum tipo α e possui referências a subárvoresleft
e opcionaisright
do mesmo tipo.uma função
height
que retorna a maior distância de qualquer nó folha ao nó raizt
.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: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: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 tipoObject
.Resumo:
fonte
Object
é bastante interessante: embora ambos sejam semelhantes, pois permitem escrever código estaticamente independente de tipo, o primeiro (genéricos) não apenas joga fora quase todas as informações de tipo disponíveis para alcançar esse objetivo. Nesse sentido particular, os genéricos são um remédio para aObject
IMO.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
é umuniversal type
e?
representa umexistential type
??
tipo inint height(Tree<?> t)
ainda não é conhecido dentro da função e ainda é determinado pelo responsável pela chamada porque foi o responsável pela escolha da árvore a ser transmitida. Mesmo se as pessoas o chamarem de tipo existencial em Java, não é. O?
espaço reservado pode ser usado para implementar uma forma de existencial em Java, em algumas circunstâncias, mas esse não é um deles.Todos esses são bons exemplos, mas opto por responder um pouco diferente. Lembre-se de matemática, que ∀x. P (x) significa "para todos os x, posso provar que P (x)". Em outras palavras, é um tipo de função, você me dá um x e eu tenho um método para provar isso para você.
Na teoria dos tipos, não estamos falando de provas, mas de tipos. Portanto, neste espaço, queremos dizer "para qualquer tipo X que você me der, eu lhe darei um tipo específico P". Agora, como não fornecemos a P muitas informações sobre X, além de ser um tipo, P não pode fazer muito com ele, mas existem alguns exemplos. P pode criar o tipo de "todos os pares do mesmo tipo":
P<X> = Pair<X, X> = (X, X)
. Ou podemos criar o tipo de opçãoP<X> = Option<X> = X | Nil
:, onde Nil é o tipo dos ponteiros nulos. Podemos fazer uma lista com isso:List<X> = (X, List<X>) | Nil
. Observe que o último é recursivo, os valores deList<X>
são pares onde o primeiro elemento é um X e o segundo elemento é umList<X>
ou então é um ponteiro nulo.Agora, em matemática ∃x. P (x) significa "Eu posso provar que existe um x específico que P (x) é verdadeiro". Pode haver muitos desses x's, mas para provar isso, basta um. Outra maneira de pensar é que deve existir um conjunto não vazio de pares de evidências e provas {(x, P (x))}.
Traduzido para a teoria dos tipos: Um tipo na família
∃X.P<X>
é um tipo X e um tipo correspondenteP<X>
. Observe que, antes de darmos X a P (para que soubéssemos tudo sobre X, mas P muito pouco), o oposto é verdadeiro agora.P<X>
não promete fornecer nenhuma informação sobre o X, apenas que existe um e que é realmente um tipo.Como isso é útil? Bem, P pode ser um tipo que exiba seu tipo interno X. Um exemplo seria um objeto que oculta a representação interna de seu estado X. Embora não tenhamos como manipulá-lo diretamente, podemos observar seu efeito cutucando P. Poderia haver muitas implementações desse tipo, mas você poderia usar todos esses tipos, independentemente de qual deles foi escolhido.
fonte
P<X>
não umaP
(mesma funcionalidade e tipo de contêiner, digamos, mas você não sabe que ela contémX
))?∀x. P(x)
não significa nada sobre a provabilidade deP(x)
, apenas a verdade.Para responder diretamente à sua pergunta:
Com o tipo universal, os usos de
T
devem incluir o parâmetro typeX
. Por exemploT<String>
ouT<Integer>
. Para o tipo existencial, o uso deT
não inclui esse parâmetro de tipo porque é desconhecido ou irrelevante - basta usarT
(ou em JavaT<?>
).Outras informações:
Tipos universais / abstratos e tipos existenciais são uma dualidade de perspectiva entre o consumidor / cliente de um objeto / função e o produtor / implementação dele. Quando um lado vê um tipo universal, o outro vê um tipo existencial.
Em Java, você pode definir uma classe genérica:
MyClass
,T
é universal porque você pode substituir qualquer tipoT
ao usar essa classe e deve conhecer o tipo real de T sempre que usar uma instância deMyClass
MyClass
si,T
é existencial porque não sabe o tipo real deT
?
representa o tipo existencial - portanto, quando você está dentro da classe,T
é basicamente?
. Se você deseja manipular uma instância deMyClass
comT
existencial, pode declararMyClass<?>
como nosecretMessage()
exemplo acima.Às vezes, tipos existentes são usados para ocultar os detalhes de implementação de algo, conforme discutido em outro lugar. Uma versão Java disso pode se parecer com:
É um pouco complicado capturar isso corretamente, porque estou fingindo estar em algum tipo de linguagem de programação funcional, que Java não é. Mas o ponto aqui é que você está capturando algum tipo de estado mais uma lista de funções que operam nesse estado e você não conhece o tipo real da parte do estado, mas as funções funcionam desde que já foram correspondidas com esse tipo .
Agora, em Java, todos os tipos não primitivos não finais são parcialmente existenciais. Isso pode parecer estranho, mas como uma variável declarada como
Object
potencialmente poderia ser uma subclasseObject
, você não pode declarar o tipo específico, apenas "esse tipo ou subclasse". E assim, os objetos são representados como um pouco de estado, além de uma lista de funções que operam nesse estado - exatamente qual função chamar é determinada em tempo de execução pela pesquisa. É muito parecido com o uso de tipos existenciais acima, onde você tem uma parte do estado existencial e uma função que opera nesse estado.Nas linguagens de programação de tipo estatístico, sem subtipagem e conversão, os tipos existenciais permitem gerenciar listas de objetos de tipos diferentes. Uma lista de
T<Int>
não pode conter aT<Long>
. No entanto, uma lista deT<?>
pode conter qualquer variação deT
, permitindo colocar muitos tipos diferentes de dados na lista e convertê-los em int (ou executar quaisquer operações fornecidas na estrutura de dados) sob demanda.Pode-se praticamente sempre converter um registro com um tipo existencial em um registro sem usar fechamentos. Um fechamento também é digitado existencialmente, pois as variáveis livres sobre as quais ele é fechado estão ocultas do chamador. Assim, uma linguagem que suporta fechamentos, mas não tipos existenciais, pode permitir fechamentos que compartilham o mesmo estado oculto que você colocaria na parte existencial de um objeto.
fonte
Um tipo existencial é um tipo opaco.
Pense em um identificador de arquivo no Unix. Você sabe que seu tipo é int, para poder falsificá-lo facilmente. Você pode, por exemplo, tentar ler do identificador 43. Se acontecer que o programa tenha um arquivo aberto com esse identificador específico, você o lerá. Seu código não precisa ser malicioso, apenas desleixado (por exemplo, o identificador pode ser uma variável não inicializada).
Um tipo existencial está oculto no seu programa. Se
fopen
um tipo existencial retornasse, tudo o que você poderia fazer seria usá-lo com algumas funções de biblioteca que aceitassem esse tipo existencial. Por exemplo, o seguinte pseudo-código seria compilado:A interface "read" é declarada como:
Existe um tipo T tal que:
A variável exfile não é
char*
um arquivo int, nem um , nem um struct - nada que você possa expressar no sistema de tipos. Você não pode declarar uma variável cujo tipo é desconhecido e não pode converter, por exemplo, um ponteiro para esse tipo desconhecido. O idioma não vai deixar você.fonte
read
for∃T.read(T file, ...)
, não há nada que você possa transmitir como o primeiro parâmetro. O que iria trabalhar é terfopen
devolver o identificador de arquivo e uma função de leitura escopo pelo mesmo existencial :∃T.(T, read(T file, ...))
Parece que estou chegando um pouco atrasado, mas, de qualquer forma, este documento adiciona outra visão do que são tipos existenciais, embora não especificamente agnósticos, deve ser bastante mais fácil entender os tipos existenciais: http: //www.cs.uu .nl / groups / ST / Projetos / ehc / ehc-book.pdf (capítulo 8)
fonte
Existe um tipo universal para todos os valores do (s) parâmetro (s) do tipo. Um tipo existencial existe apenas para valores do (s) parâmetro (s) do tipo que atendem às restrições do tipo existencial.
Por exemplo, no Scala, uma maneira de expressar um tipo existencial é um tipo abstrato que é restrito a alguns limites superiores ou inferiores.
Equivalentemente, um tipo universal restrito é um tipo existencial, como no exemplo a seguir.
Qualquer site de uso pode empregar o
Interface
porque qualquer subtipo instável deExistential
deve definir otype Parameter
que deve ser implementadoInterface
.Um caso degenerado de um tipo existencial em Scala é um tipo abstrato que nunca é referido e, portanto, não precisa ser definido por nenhum subtipo. Isso efetivamente possui uma notação abreviada
List[_]
no Scala eList<?>
no Java.Minha resposta foi inspirada na proposta de Martin Odersky de unificar tipos abstratos e existenciais. O slide que acompanha auxilia a compreensão.
fonte
∀x.f(x)
, são opacos às suas funções de recebimento, enquanto os Tipos Existenciais∃x.f(x)
, são limitados a ter certas propriedades. Normalmente, todos os parâmetros são Existenciais, pois sua função os manipulará diretamente; no entanto, parâmetros genéricos podem ter tipos que são universais, pois a função não os gerenciará além das operações universais básicas, como a obtenção de uma referência como em:∀x.∃array.copy(src:array[x] dst:array[x]){...}
forSome
para quantificação existencial do parâmetro type.Pesquisas sobre tipos de dados abstratos e ocultação de informações trouxeram tipos existenciais para linguagens de programação. Tornar um tipo de dados abstrato oculta informações sobre esse tipo, para que um cliente desse tipo não possa abusar dele. Digamos que você tenha uma referência a um objeto ... alguns idiomas permitem que você faça essa referência a uma referência a bytes e faça o que quiser com esse pedaço de memória. Para garantir o comportamento de um programa, é útil para uma linguagem impor que você só atue na referência ao objeto por meio dos métodos fornecidos pelo designer do objeto. Você sabe que o tipo existe, mas nada mais.
fonte
Eu criei este diagrama. Não sei se é rigoroso. Mas se ajudar, fico feliz.
fonte
Pelo que entendi, é uma maneira matemática de descrever interfaces / classe abstrata.
Quanto a T = ∃X {X a; int f (X); }
Para C #, seria traduzido para um tipo abstrato genérico:
"Existencial" significa apenas que existe algum tipo que obedece às regras definidas aqui.
fonte