web-dev-qa-db-fra.com

Qu'est-ce qu'un type existentiel?

J'ai lu l'article Wikipedia Types existentiels. J'ai compris qu'ils sont appelés types existentiels en raison de l'opérateur existentiel (∃). Mais je ne sais pas à quoi ça sert. Quelle est la différence entre

T = ∃X { X a; int f(X); }

et

T = ∀x { X a; int f(X); }

?

158
Claudiu

Lorsque quelqu'un définit un type universel ∀X ils disent: Vous pouvez brancher le type que vous voulez, je n'ai pas besoin de savoir quoi que ce soit sur le type pour faire mon travail, je vais seulement y faire référence de manière opaque comme X.

Lorsque quelqu'un définit un type existentiel ∃X ils disent: J'utiliserai le type que je veux ici; vous ne savez rien du type, vous ne pouvez donc vous y référer que de manière opaque X.

Les types universels vous permettent d'écrire des choses comme:

void copy<T>(List<T> source, List<T> dest) {
   ...
}

La fonction copy n'a aucune idée de ce que T sera réellement, mais elle n'en a pas besoin.

Les types existentiels vous permettraient d'écrire des choses comme:

interface VirtualMachine<B> {
   B compile(String source);
   void run(B bytecode);
}

// Now, if you had a list of VMs you wanted to run on the same input:
void runAllCompilers(List<∃B:VirtualMachine<B>> vms, String source) {
   for (∃B:VirtualMachine<B> vm : vms) {
      B bytecode = vm.compile(source);
      vm.run(bytecode);
   }
}

Chaque implémentation de machine virtuelle dans la liste peut avoir un type de bytecode différent. La fonction runAllCompilers n'a aucune idée du type de bytecode, mais elle n'en a pas besoin; il ne fait que relayer le bytecode de VirtualMachine.compile à VirtualMachine.run.

Caractères génériques de type Java (ex: List<?>) sont une forme très limitée de types existentiels.

pdate: J'ai oublié de mentionner que vous pouvez trier des types existentiels avec des types universels. Commencez par envelopper votre type universel pour masquer le paramètre de type. Deuxièmement, inverser le contrôle (cela échange efficacement la partie "vous" et "je" dans les définitions ci-dessus, qui est la principale différence entre les existentiels et les universaux).

// A wrapper that hides the type parameter 'B'
interface VMWrapper {
   void unwrap(VMHandler handler);
}

// A callback (control inversion)
interface VMHandler {
   <B> void handle(VirtualMachine<B> vm);
}

Maintenant, nous pouvons avoir le VMWrapper appeler notre propre VMHandler qui a une fonction handle universellement typée. L'effet net est le même, notre code doit traiter B comme opaque.

void runWithAll(List<VMWrapper> vms, final String input)
{
   for (VMWrapper vm : vms) {
      vm.unwrap(new VMHandler() {
         public <B> void handle(VirtualMachine<B> vm) {
            B bytecode = vm.compile(input);
            vm.run(bytecode);
         }
      });
   }
}

Un exemple VM implémentation:

class MyVM implements VirtualMachine<byte[]>, VMWrapper {
   public byte[] compile(String input) {
      return null; // TODO: somehow compile the input
   }
   public void run(byte[] bytecode) {
      // TODO: Somehow evaluate 'bytecode'
   }
   public void unwrap(VMHandler handler) {
      handler.handle(this);
   }
}
172
Kannan Goundan

Une valeur de un type existentiel comme ∃x. F(x) est une paire contenant un typex et un value du type F(x). Alors qu'une valeur d'un type polymorphe comme ∀x. F(x) est une fonction qui prend du type x et produit une valeur de type F(x). Dans les deux cas, le type se ferme sur un constructeur de type F.

Notez que cette vue mélange les types et les valeurs. La preuve existentielle est un type et une valeur. La preuve universelle est une famille entière de valeurs indexées par type (ou un mappage des types aux valeurs).

Ainsi, la différence entre les deux types que vous avez spécifiés est la suivante:

T = ∃X { X a; int f(X); }

Cela signifie: Une valeur de type T contient un type appelé X, une valeur a:X, et une fonction f:X->int. Un producteur de valeurs de type T doit choisir any type pour X et un consommateur ne peut rien savoir de X. Sauf qu'il en existe un exemple appelé a et que cette valeur peut être transformée en int en la donnant à f. En d'autres termes, une valeur de type T sait comment produire un int d'une manière ou d'une autre. Eh bien, nous pourrions éliminer le type intermédiaire X et dire simplement:

T = int

Celui universellement quantifié est un peu différent.

T = ∀X { X a; int f(X); }

Cela signifie: Une valeur de type T peut être affectée à n'importe quel type X, et elle produira une valeur a:X, et une fonction f:X->int peu importe ce que X est. En d'autres termes: un consommateur de valeurs de type T peut choisir n'importe quel type pour X. Et un producteur de valeurs de type T ne peut rien savoir de X, mais il doit être capable de produire une valeur a pour tout choix de X, et pouvoir transformer une telle valeur en int.

Évidemment, l'implémentation de ce type est impossible, car aucun programme ne peut produire une valeur de chaque type imaginable. Sauf si vous autorisez des absurdités comme null ou des fonds.

Puisqu'un existentiel est une paire, un argument existentiel peut être converti en un universel via currying .

(∃b. F(b)) -> Int

est le même que:

∀b. (F(b) -> Int)

Le premier est un existentiel de rang 2 . Cela conduit à la propriété utile suivante:

Chaque type de rang existentiellement quantifié n+1 est un type de rang universellement quantifié n.

Il existe un algorithme standard pour transformer les existentiels en universaux, appelé Skolemization .

97
Apocalisp

Je pense qu'il est logique d'expliquer les types existentiels avec les types universels, car les deux concepts sont complémentaires, c'est-à-dire que l'un est "l'opposé" de l'autre.

Je ne peux pas répondre à tous les détails sur les types existentiels (comme donner une définition exacte, énumérer toutes les utilisations possibles, leur relation avec les types de données abstraits, etc.) parce que je ne suis tout simplement pas suffisamment informé pour cela. Je montrerai seulement (en utilisant Java) ce que cet article de HaskellWiki déclare être le principal effet des types existentiels:

Les types existentiels peuvent être utilisés à plusieurs fins différentes. Mais qu'est-ce qu'ils faire est de "masquer" une variable de type sur le côté droit. Normalement, toute variable de type apparaissant à droite doit également apparaître à gauche […]

Exemple de configuration:

Le pseudo-code suivant n'est pas Java tout à fait valide, même s'il serait assez facile de résoudre ce problème. En fait, c'est exactement ce que je vais faire dans cette réponse!

class Tree<α>
{
    α       value;
    Tree<α> left;
    Tree<α> right;
}

int height(Tree<α> t)
{
    return (t != null)  ?  1 + max( height(t.left), height(t.right) )
                        :  0;
}

Permettez-moi de vous l'expliquer brièvement. Nous définissons…

  • un type récursif Tree<α> qui représente un nœud dans un arbre binaire. Chaque nœud stocke un value d'un certain type α et a des références aux sous-arbres left et right facultatifs du même type.

  • une fonction height qui renvoie la distance la plus éloignée de tout nœud feuille au nœud racine t.

Maintenant, transformons le pseudo-code ci-dessus pour height en une bonne syntaxe Java! (Je continuerai à omettre un passe-partout par souci de concision, comme l'orientation des objets et l'accessibilité) modificateurs.) Je vais montrer deux solutions possibles.

1. Solution de type universel:

La solution la plus évidente consiste simplement à rendre height générique en introduisant le paramètre de type α dans sa signature:

<α> int height(Tree<α> t)
{
    return (t != null)  ?  1 + max( height(t.left), height(t.right) )
                        :  0;
}

Cela vous permettrait de déclarer des variables et de créer des expressions de type α à l'intérieur de cette fonction, si vous le vouliez. Mais...

2. Solution de type existentiel:

Si vous regardez le corps de notre méthode, vous remarquerez que nous n'accédons pas ou ne travaillons pas à quoi que ce soit de type α! Il n'y a aucune expression ayant ce type, ni aucune variable déclarée avec ce type ... alors, pourquoi devons-nous rendre générique height? Pourquoi ne pouvons-nous pas simplement oublier α? Il s'avère que nous pouvons:

int height(Tree<?> t)
{
    return (t != null)  ?  1 + max( height(t.left), height(t.right) )
                        :  0;
}

Comme je l'ai écrit au tout début de cette réponse, les types existentiels et universels sont de nature complémentaire/double. Ainsi, si la solution de type universel devait rendre générique height plus, alors nous devrions nous attendre à ce que les types existentiels aient l'effet inverse: en faisant moins générique, à savoir en masquant/supprimant le paramètre de type α.

Par conséquent, vous ne pouvez plus faire référence au type de t.value dans cette méthode, ni manipuler d'expressions de ce type, car aucun identifiant n'y a été lié. (Le ? joker est un jeton spécial, pas un identifiant qui "capture" un type.) t.value est effectivement devenu opaque; peut-être que la seule chose que vous pouvez encore en faire est de le transtyper en Object.

Résumé:

===========================================================
                     |    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   |                  
=====================+=====================================
31
stakx

Ce sont tous de bons exemples, mais j'ai choisi d'y répondre un peu différemment. Rappelez-vous des mathématiques, que ∀x. P(x) signifie "pour tous les x, je peux prouver que P (x)". En d'autres termes, c'est une sorte de fonction, vous me donnez un x et j'ai une méthode pour le prouver pour vous.

Dans la théorie des types, nous ne parlons pas de preuves, mais de types. Donc, dans cet espace, nous voulons dire "pour tout type X que vous me donnez, je vous donnerai un type P spécifique". Maintenant, comme nous ne donnons pas beaucoup d'informations à P sur X à part le fait qu'il s'agit d'un type, P ne peut pas en faire grand-chose, mais il y a quelques exemples. P peut créer le type de "toutes les paires du même type": P<X> = Pair<X, X> = (X, X). Ou nous pouvons créer le type d'option: P<X> = Option<X> = X | Nil, Où Nil est le type des pointeurs nuls. Nous pouvons en faire une liste: List<X> = (X, List<X>) | Nil. Notez que le dernier est récursif, les valeurs de List<X> Sont soit des paires où le premier élément est un X et le deuxième élément est un List<X> Ou bien il s'agit d'un pointeur nul.

Maintenant, en mathématiques ∃x. P(x) signifie "je peux prouver qu'il y a un x particulier tel que P(x) est vrai". Il peut y avoir beaucoup de tels x, mais pour le prouver, une seule suffit. Une autre façon de penser est qu'il doit exister un ensemble non vide de couples preuves-preuves {(x, P (x))}.

Traduit en théorie des types: Un type de la famille ∃X.P<X> Est un type X et un type correspondant P<X>. Remarquez qu'avant d'avoir donné X à P, (de sorte que nous savions tout sur X mais P très peu), l'inverse est vrai maintenant. P<X> Ne promet pas de donner des informations sur X, juste qu'il y en a une, et qu'il s'agit bien d'un type.

Comment est-ce utile? Eh bien, P pourrait être un type qui a une façon d'exposer son type interne X. Un exemple serait un objet qui cache la représentation interne de son état X. Bien que nous n'ayons aucun moyen de le manipuler directement, nous pouvons observer son effet en piquer à P. Il pourrait y avoir de nombreuses implémentations de ce type, mais vous pouvez utiliser tous ces types, peu importe lequel en particulier a été choisi.

14
Rogon

Un type existentiel est un type opaque.

Pensez à un descripteur de fichier sous Unix. Vous savez que son type est int, vous pouvez donc le forger facilement. Vous pouvez, par exemple, essayer de lire à partir du descripteur 43. S'il arrive que le programme ait un fichier ouvert avec ce descripteur particulier, vous le lirez. Votre code n'a pas besoin d'être malveillant, juste bâclé (par exemple, le handle peut être une variable non initialisée).

Un type existentiel est masqué de votre programme. Si fopen a renvoyé un type existentiel, tout ce que vous pourriez en faire est de l'utiliser avec certaines fonctions de bibliothèque qui acceptent ce type existentiel. Par exemple, le pseudo-code suivant se compilerait:

let exfile = fopen("foo.txt"); // No type for exfile!
read(exfile, buf, size);

L'interface "read" est déclarée comme:

Il existe un type T tel que:

size_t read(T exfile, char* buf, size_t size);

La variable exfile n'est pas un int, pas un char*, pas un fichier struct - rien que vous puissiez exprimer dans le système de type. Vous ne pouvez pas déclarer une variable dont le type est inconnu et vous ne pouvez pas transposer, par exemple, un pointeur dans ce type inconnu. La langue ne vous laissera pas.

12
Bartosz Milewski

Pour répondre directement à votre question:

Avec le type universel, les utilisations de T doivent inclure le paramètre de type X. Par exemple T<String> Ou T<Integer>. Pour le type existentiel, les utilisations de T n'incluent pas ce paramètre de type car il est inconnu ou non pertinent - utilisez simplement T (ou dans Java T<?>).

Plus d'informations:

Les types universels/abstraits et les types existentiels sont une dualité de perspective entre le consommateur/client d'un objet/fonction et le producteur/implémentation de celui-ci. Lorsqu'un côté voit un type universel, l'autre voit un type existentiel.

Dans Java vous pouvez définir une classe générique:

public class MyClass<T> {
   // T is existential in here
   T whatever; 
   public MyClass(T w) { this.whatever = w; }

   public static MyClass<?> secretMessage() { return new MyClass("bazzlebleeb"); }
}

// T is universal from out here
MyClass<String> mc1 = new MyClass("foo");
MyClass<Integer> mc2 = new MyClass(123);
MyClass<?> mc3 = MyClass.secretMessage();
  • Du point de vue d'un client de MyClass, T est universel car vous pouvez remplacer n'importe quel type par T lorsque vous utilisez cette classe et vous devez connaître le type réel de T chaque fois que vous utilisez une instance de MyClass
  • Du point de vue des méthodes d'instance dans MyClass lui-même, T est existentiel car il ne connaît pas le véritable type de T
  • En Java, ? Représente le type existentiel - ainsi lorsque vous êtes dans la classe, T est fondamentalement ?. Si vous voulez gérer une instance de MyClass avec T existentielle, vous pouvez déclarer MyClass<?> Comme dans l'exemple secretMessage() ci-dessus.

Les types existentiels sont parfois utilisés pour masquer les détails d'implémentation de quelque chose, comme discuté ailleurs. Une Java de ceci pourrait ressembler à:

public class ToDraw<T> {
    T obj;
    Function<Pair<T,Graphics>, Void> draw;
    ToDraw(T obj, Function<Pair<T,Graphics>, Void>
    static void draw(ToDraw<?> d, Graphics g) { d.draw.apply(new Pair(d.obj, g)); }
}

// Now you can put these in a list and draw them like so:
List<ToDraw<?>> drawList = ... ;
for(td in drawList) ToDraw.draw(td);

Il est un peu difficile de capturer cela correctement parce que je fais semblant d'être dans une sorte de langage de programmation fonctionnel, ce qui n'est pas le cas Java. Mais le point ici est que vous capturez une sorte de état plus une liste de fonctions qui opèrent sur cet état et vous ne connaissez pas le type réel de la partie état, mais les fonctions le font puisqu'elles étaient déjà associées à ce type.

Maintenant, en Java tous les types non finaux non primitifs sont partiellement existentiels. Cela peut sembler étrange, mais parce qu'une variable déclarée comme Object pourrait potentiellement être une sous-classe de Object à la place, vous ne pouvez pas déclarer le type spécifique, seulement "ce type ou une sous-classe". Ainsi, les objets sont représentés comme un peu d'état plus une liste de fonctions qui opèrent sur cet état - exactement quelle fonction appeler est déterminé lors de l'exécution par la recherche. Cela ressemble beaucoup à l'utilisation des types existentiels ci-dessus où vous avez une partie d'état existentiel et une fonction qui fonctionne sur cet état.

Dans les langages de programmation typés statiquement sans sous-typage ni transtypage, les types existentiels permettent de gérer des listes d'objets typés différemment. Une liste de T<Int> Ne peut pas contenir un T<Long>. Cependant, une liste de T<?> Peut contenir n'importe quelle variation de T, ce qui permet de mettre de nombreux types de données différents dans la liste et de les convertir tous en un int (ou d'effectuer toutes les opérations fournies à l'intérieur la structure des données) sur demande.

On peut presque toujours convertir un enregistrement de type existentiel en un enregistrement sans utiliser de fermetures. Une fermeture est typiquement existentielle également, en ce sens que les variables libres sur lesquelles elle est fermée sont cachées à l'appelant. Ainsi, un langage qui prend en charge les fermetures mais pas les types existentiels peut vous permettre de créer des fermetures qui partagent le même état caché que vous auriez mis dans la partie existentielle d'un objet.

11
Dobes Vandermeer

Il semble que j'arrive un peu en retard, mais de toute façon, ce document ajoute une autre vue de ce que sont les types existentiels, bien qu'il ne soit pas spécifiquement indépendant du langage, il devrait être alors assez facile de comprendre les types existentiels: http: // www .cs.uu.nl/groups/ST/Projects/ehc/ehc-book.pdf (chapitre 8)

La différence entre un type quantifié universellement et existentiellement peut être caractérisée par l'observation suivante:

  • L'utilisation d'une valeur de type ∀ quantifié détermine le type à choisir pour l'instanciation de la variable de type quantifié. Par exemple, l'appelant de la fonction d'identité "id :: ∀a.a → a" détermine le type à choisir pour la variable de type a pour cette application particulière de id. Pour l'application de fonction "id 3", ce type est égal à Int.

  • La création d'une valeur de type ∃ quantifié détermine et masque le type de la variable de type quantifié. Par exemple, un créateur d'un "∃a. (A, a → Int)" peut avoir construit une valeur de ce type à partir de "(3, λx → x)"; un autre créateur a construit une valeur du même type à partir de "(" x ", λx → ord x)". Du point de vue des utilisateurs, les deux valeurs ont le même type et sont donc interchangeables. La valeur a un type spécifique choisi pour la variable de type a, mais nous ne savons pas quel type, donc ces informations ne peuvent plus être exploitées. Cette information de type spécifique à une valeur a été "oubliée"; nous savons seulement qu'il existe.

6
themarketka

Un type universel existe pour toutes les valeurs du ou des paramètres de type. Un type existentiel existe uniquement pour les valeurs du ou des paramètres de type qui satisfont aux contraintes du type existentiel.

Par exemple, dans Scala une façon d'exprimer un type existentiel est un type abstrait qui est contraint à certaines limites supérieures ou inférieures.

trait Existential {
  type Parameter <: Interface
}

De manière équivalente, un type universel contraint est un type existentiel comme dans l'exemple suivant.

trait Existential[Parameter <: Interface]

Tout site d'utilisation peut utiliser le Interface car tout sous-type instanciable de Existential doit définir le type Parameter qui doit implémenter le Interface.

Un cas dégénéré d'un type existentiel dans Scala est un type abstrait qui n'est jamais mentionné et donc n'a pas besoin d'être défini par n'importe quel sous-type. Ceci a effectivement une notation abrégée de List[_]dans Scala et List<?> en Java.

Ma réponse a été inspirée par la proposition de Martin Odersky d'unifier les types abstraits et existentiels. La diapositive d'accompagnement facilite la compréhension.

4
Shelby Moore III

La recherche sur les types de données abstraits et la dissimulation d'informations a introduit des types existentiels dans les langages de programmation. Faire un résumé de type de données cache des informations sur ce type, donc un client de ce type ne peut pas en abuser. Disons que vous avez une référence à un objet ... certains langages vous permettent de convertir cette référence en une référence en octets et de faire tout ce que vous voulez sur ce morceau de mémoire. Dans le but de garantir le comportement d'un programme, il est utile pour un langage d'imposer que vous n'agissez que sur la référence à l'objet via les méthodes fournies par le concepteur de l'objet. Vous savez que le type existe, mais rien de plus.

Voir:

Les types abstraits ont un type existentiel, MITCHEL & PLOTKIN

http://theory.stanford.edu/~jcm/papers/mitch-plotkin-88.pdf

3
ja.

J'ai créé ce diagramme. Je ne sais pas si c'est rigoureux. Mais si ça aide, je suis content. enter image description here

1
v.oddou