ChatGPT解决这个技术问题 Extra ChatGPT

What is an existential type?

I read through the Wikipedia article Existential types. I gathered that they're called existential types because of the existential operator (∃). I'm not sure what the point of it is, though. What's the difference between

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

and

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

?

You're asking what the difference is between "there is some dog" and "everything is a dog".

D
Dan Zheng

When someone defines a universal type ∀X they're saying: You can plug in whatever type you want, I don't need to know anything about the type to do my job, I'll only refer to it opaquely as X.

When someone defines an existential type ∃X they're saying: I'll use whatever type I want here; you won't know anything about the type, so you can only refer to it opaquely as X.

Universal types let you write things like:

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

The copy function has no idea what T will actually be, but it doesn't need to know.

Existential types would let you write things like:

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);
   }
}

Each virtual machine implementation in the list can have a different bytecode type. The runAllCompilers function has no idea what the bytecode type is, but it doesn't need to; all it does is relay the bytecode from VirtualMachine.compile to VirtualMachine.run.

Java type wildcards (ex: List<?>) are a very limited form of existential types.

Update: Forgot to mention that you can sort of simulate existential types with universal types. First, wrap your universal type to hide the type parameter. Second, invert control (this effectively swaps the "you" and "I" part in the definitions above, which is the primary difference between existentials and universals).

// 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);
}

Now, we can have the VMWrapper call our own VMHandler which has a universally-typed handle function. The net effect is the same, our code has to treat B as 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);
         }
      });
   }
}

An example VM implementation:

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);
   }
}

@Kannan, +1 for a very useful, but somewhat difficult-to-grasp answer: 1. I think it would help if you could be more explicit about the dual nature of existential & universal types. I only realised by accident how you've phrased the first two paragraphs very similarly; only later do you state explicitly that both definitions are basically the same, but with "I" and "you" reversed. Also, I didn't immediately understand what "I" and "you" is supposed to refer to.
(continued:) 2. I don't fully understand the meaning of the mathematical notation in List<∃B:VirtualMachine<B>> vms or for (∃B:VirtualMachine<B> vm : vms). (Since these are generic types, couldn't you have used Java's ? wildcards instead of "self-made" syntax?) I think it might help to have a code example where no generic types such as ∃B:VirtualMachine<B> are involved, but instead a "straight" ∃B, because generic types are easily associated with universal types after your first code examples.
I used ∃B to be explicit about where the quantification is happening. With wildcard syntax the quantifier is implied (List<List<?>> actually means ∃T:List<List<T>> and not List<∃T:List<T>>). Also, explicit quantification lets you refer to the type (I modified the example to take advantage of this by storing the bytecode of type B in a temporary variable).
The mathematical notation used here is as sloppy as hell, but I don't think that's the answerer's fault (it's standard). Still, best not to abuse the existential and universal quantifiers in such a way perhaps...
@LP_ Java implicitly places the quantifier on the innermost type expression. List<List<?>> means List<∃T:List<T>>. There's no way in Java to specify ∃T:List<List<T>>. The runAllCompilers works because the limitation happens to be fine in this case.
1
16 revs

A value of an existential type like ∃x. F(x) is a pair containing some type x and a value of the type F(x). Whereas a value of a polymorphic type like ∀x. F(x) is a function that takes some type x and produces a value of type F(x). In both cases, the type closes over some type constructor F.

Note that this view mixes types and values. The existential proof is one type and one value. The universal proof is an entire family of values indexed by type (or a mapping from types to values).

So the difference between the two types you specified is as follows:

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

This means: A value of type T contains a type called X, a value a:X, and a function f:X->int. A producer of values of type T gets to choose any type for X and a consumer can't know anything about X. Except that there's one example of it called a and that this value can be turned into an int by giving it to f. In other words, a value of type T knows how to produce an int somehow. Well, we could eliminate the intermediate type X and just say:

T = int

The universally quantified one is a little different.

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

This means: A value of type T can be given any type X, and it will produce a value a:X, and a function f:X->int no matter what X is. In other words: a consumer of values of type T can choose any type for X. And a producer of values of type T can't know anything at all about X, but it has to be able to produce a value a for any choice of X, and be able to turn such a value into an int.

Obviously implementing this type is impossible, because there is no program that can produce a value of every imaginable type. Unless you allow absurdities like null or bottoms.

Since an existential is a pair, an existential argument can be converted to a universal one via currying.

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

is the same as:

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

The former is a rank-2 existential. This leads to the following useful property:

Every existentially quantified type of rank n+1 is a universally quantified type of rank n.

There is a standard algorithm for turning existentials into universals, called Skolemization.


It might be useful (or not) to mention Skolemization en.wikipedia.org/wiki/Skolem_normal_form
s
stakx - no longer contributing

I think it makes sense to explain existential types together with universal types, since the two concepts are complementary, i.e. one is the "opposite" of the other.

I cannot answer every detail about existential types (such as giving an exact definition, list all possible uses, their relation to abstract data types, etc.) because I'm simply not knowledgeable enough for that. I'll demonstrate only (using Java) what this HaskellWiki article states to be the principal effect of existential types:

Existential types can be used for several different purposes. But what they do is to 'hide' a type variable on the right-hand side. Normally, any type variable appearing on the right must also appear on the left […]

Example set-up:

The following pseudo-code is not quite valid Java, even though it would be easy enough to fix that. In fact, that's exactly what I'm going to do in this answer!

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

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

Let me briefly spell this out for you. We are defining…

a recursive type Tree<α> which represents a node in a binary tree. Each node stores a value of some type α and has references to optional left and right subtrees of the same type.

a function height which returns the furthest distance from any leaf node to the root node t.

Now, let's turn the above pseudo-code for height into proper Java syntax! (I'll keep on omitting some boilerplate for brevity's sake, such as object-orientation and accessibility modifiers.) I'm going to show two possible solutions.

1. Universal type solution:

The most obvious fix is to simply make height generic by introducing the type parameter α into its signature:

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

This would allow you to declare variables and create expressions of type α inside that function, if you wanted to. But...

2. Existential type solution:

If you look at our method's body, you will notice that we're not actually accessing, or working with, anything of type α! There are no expressions having that type, nor any variables declared with that type... so, why do we have to make height generic at all? Why can't we simply forget about α? As it turns out, we can:

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

As I wrote at the very beginning of this answer, existential and universal types are complementary / dual in nature. Thus, if the universal type solution was to make height more generic, then we should expect that existential types have the opposite effect: making it less generic, namely by hiding/removing the type parameter α.

As a consequence, you can no longer refer to the type of t.value in this method nor manipulate any expressions of that type, because no identifier has been bound to it. (The ? wildcard is a special token, not an identifier that "captures" a type.) t.value has effectively become opaque; perhaps the only thing you can still do with it is type-cast it to Object.

Summary:

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

Good explanation. You don't need to cast t.value to Object, you can just refer to it as Object. I would say that the existential type makes the method more generic because of that. The only thing you can ever know about t.value is that it's an Object whereas you could have said something specific about α (like α extends Serializable).
I've meanwhile come to believe that my answer doesn't really explain what existentials are, and I'm considering writing another one that's more like the first two paragraphs of Kannan Goudan's answer, which I think is closer to the "truth". That being said, @Craig: Comparing generics with Object is quite interesting: While both are similar in that they enable you to write statically type-independent code, the former (generics) doesn't just throw away almost all of the available type information to achieve this goal. In this particular sense, generics are a remedy to Object IMO.
@stakx, in this code (from Effective Java) 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 is a universal type and ? represents an existential type?
This answer is not correct. The ? in the type int height(Tree<?> t) is still not known inside the function, and is still determined by the caller because it is the caller that got to choose which tree to pass in. Even if people call this the existential type in Java, it is not. The ? placeholder can be used to implement a form of existentials in Java, in some circumstances, but this is not one of them.
R
Rogon

These are all good examples, but I choose to answer it a little bit differently. Recall from math, that ∀x. P(x) means "for all x's, I can prove that P(x)". In other words, it is a kind of function, you give me an x and I have a method to prove it for you.

In type theory, we are not talking about proofs, but of types. So in this space we mean "for any type X you give me, I will give you a specific type P". Now, since we don't give P much information about X besides the fact that it is a type, P can't do much with it, but there are some examples. P can create the type of "all pairs of the same type": P<X> = Pair<X, X> = (X, X). Or we can create the option type: P<X> = Option<X> = X | Nil, where Nil is the type of the null pointers. We can make a list out of it: List<X> = (X, List<X>) | Nil. Notice that the last one is recursive, values of List<X> are either pairs where the first element is an X and the second element is a List<X> or else it is a null pointer.

Now, in math ∃x. P(x) means "I can prove that there is a particular x such that P(x) is true". There may be many such x's, but to prove it, one is enough. Another way to think of it is that there must exist a non-empty set of evidence-and-proof pairs {(x, P(x))}.

Translated to type theory: A type in the family ∃X.P<X> is a type X and a corresponding type P<X>. Notice that while before we gave X to P, (so that we knew everything about X but P very little) that the opposite is true now. P<X> doesn't promise to give any information about X, just that there there is one, and that it is indeed a type.

How is this useful? Well, P could be a type that has a way of exposing its internal type X. An example would be an object which hides the internal representation of its state X. Though we have no way of directly manipulating it, we can observe its effect by poking at P. There could be many implementations of this type, but you could use all of these types no matter which particular one was chosen.


Hmm but what does the function gain by knowing it is a P<X> instead of a P (same functionality and container type, let's say, but you don't know it contains X)?
Strictly speaking, ∀x. P(x) does not mean anything about the provability of P(x), only the truth.
"∀x. P(x) means "for all x's, I can prove that P(x)". " -- Um, no, that's not at all what it means. It's an assertion that P(x) for all x. "In other words, it is a kind of function, you give me an x and I have a method to prove it for you." -- these are nonsense words.
D
Dobes Vandermeer

To directly answer your question:

With the universal type, uses of T must include the type parameter X. For example T<String> or T<Integer>. For the existential type uses of T do not include that type parameter because it is unknown or irrelevant - just use T (or in Java T<?>).

Further information:

Universal/abstract types and existential types are a duality of perspective between the consumer/client of an object/function and the producer/implementation of it. When one side sees a universal type the other sees an existential type.

In Java you can define a generic class:

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();

From the perspective of a client of MyClass, T is universal because you can substitute any type for T when you use that class and you must know the actual type of T whenever you use an instance of MyClass

From the perspective of instance methods in MyClass itself, T is existential because it doesn't know the real type of T

In Java, ? represents the existential type - thus when you are inside the class, T is basically ?. If you want to handle an instance of MyClass with T existential, you can declare MyClass as in the secretMessage() example above.

Existential types are sometimes used to hide the implementation details of something, as discussed elsewhere. A Java version of this might look like:

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);

It's a bit tricky to capture this properly because I'm pretending to be in some sort of functional programming language, which Java isn't. But the point here is that you are capturing some sort of state plus a list of functions that operate on that state and you don't know the real type of the state part, but the functions do since they were matched up with that type already.

Now, in Java all non-final non-primitive types are partly existential. This may sound strange, but because a variable declared as Object could potentially be a subclass of Object instead, you cannot declare the specific type, only "this type or a subclass". And so, objects are represented as a bit of state plus a list of functions that operate on that state - exactly which function to call is determined at runtime by lookup. This is very much like the use of existential types above where you have an existential state part and a function that operates on that state.

In statically typed programming languages without subtyping and casts, existential types allow one to manage lists of differently typed objects. A list of T<Int> cannot contain a T<Long>. However, a list of T<?> can contain any variation of T, allowing one to put many different types of data into the list and convert them all to an int (or do whatever operations are provided inside the data structure) on demand.

One can pretty much always convert a record with an existential type into a record without using closures. A closure is existentially typed, too, in that the free variables it is closed over are hidden from the caller. Thus a language that supports closures but not existential types can allow you to make closures that share the same hidden state that you would have put into the existential part of an object.


Great explanation! This answer deserves more upvotes.
P
Peter Hall

An existential type is an opaque type.

Think of a file handle in Unix. You know its type is int, so you can easily forge it. You can, for instance, try to read from handle 43. If it so happens that the program has a file open with this particular handle, you'll read from it. Your code doesn't have to be malicious, just sloppy (e.g., the handle could be an uninitialized variable).

An existential type is hidden from your program. If fopen returned an existential type, all you could do with it is to use it with some library functions that accept this existential type. For instance, the following pseudo-code would compile:

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

The interface "read" is declared as:

There exists a type T such that:

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

The variable exfile is not an int, not a char*, not a struct File—nothing you can express in the type system. You can't declare a variable whose type is unknown and you cannot cast, say, a pointer into that unknown type. The language won't let you.


This won't work. If the signature of read is ∃T.read(T file, ...) then there is nothing you can pass in as the first parameter. What would work is to have fopen return the file handle and a read function scoped by the same existential: ∃T.(T, read(T file, ...))
This seems to be just talking about ADTs.
"No type for exfile!" -- then how can you pass it to read? How would you distinguish it from other variables with "no type"? "nothing you can express in the type system" -- then your language design is broken.
t
themarketka

Seems I’m coming a bit late, but anyway, this document adds another view of what existential types are, although not specifically language-agnostic, it should be then fairly easier to understand existential types: http://www.cs.uu.nl/groups/ST/Projects/ehc/ehc-book.pdf (chapter 8)

The difference between a universally and existentially quantified type can be characterized by the following observation: The use of a value with a ∀ quantified type determines the type to choose for the instantiation of the quantified type variable. For example, the caller of the identity function “id :: ∀a.a → a” determines the type to choose for the type variable a for this particular application of id. For the function application “id 3” this type equals Int. The creation of a value with a ∃ quantified type determines, and hides, the type of the quantified type variable. For example, a creator of a “∃a.(a, a → Int)” may have constructed a value of that type from “(3, λx → x)”; another creator has constructed a value with the same type from “(’x’, λx → ord x)”. From a users point of view both values have the same type and are thus interchangeable. The value has a specific type chosen for type variable a, but we do not know which type, so this information can no longer be exploited. This value specific type information has been ‘forgotten’; we only know it exists.


B
Bubletan

A universal type exists for all values of the type parameter(s). An existential type exists only for values of the type parameter(s) that satisfy the constraints of the existential type.

For example in Scala one way to express an existential type is an abstract type which is constrained to some upper or lower bounds.

trait Existential {
  type Parameter <: Interface
}

Equivalently a constrained universal type is an existential type as in the following example.

trait Existential[Parameter <: Interface]

Any use site can employ the Interface because any instantiable subtypes of Existential must define the type Parameter which must implement the Interface.

A degenerate case of an existential type in Scala is an abstract type which is never referred to and thus need not be defined by any subtype. This effectively has a shorthand notation of List[_] in Scala and List<?> in Java.

My answer was inspired by Martin Odersky's proposal to unify abstract and existential types. The accompanying slide aids understanding.


Having read through some of the material above it seems you have nicely summed up my understanding: Universal Types, ∀x.f(x), are opaque to their receiving functions while Existential Types, ∃x.f(x), are constrained to having certain properties. Typically, all parameters are Existential since their function will be manipulating them directly; however, generic parameters may have types that are Universal since the function will not manage them beyond basic universal operations such as obtaining a reference as in: ∀x.∃array.copy(src:array[x] dst:array[x]){...}
As described here stackoverflow.com/a/19413755/3195266 type members can emulate universal quantification via identity type. And for sure there's forSome for type parameter existential quantification.
j
ja.

Research into abstract datatypes and information hiding brought existential types into programming languages. Making a datatype abstract hides info about that type, so a client of that type cannot abuse it. Say you've got a reference to an object... some languages allow you to cast that reference to a reference to bytes and do anything you want to that piece of memory. For purposes of guaranteeing behavior of a program, it's useful for a language to enforce that you only act on the reference to the object via the methods the designer of the object provides. You know the type exists, but nothing more.

See: Abstract Types Have Existential Type, MITCHEL & PLOTKIN http://theory.stanford.edu/~jcm/papers/mitch-plotkin-88.pdf


v
v.oddou

https://i.stack.imgur.com/hkccG.png


u
user35910

As I understand it's a math way to describe interfaces/abstract class.

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

For C# it would translate to a generic abstract type:

abstract class MyType<T>{
    private T a;

    public abstract int f(T x);
}

"Existential" just means that there is some type that obey to the rules defined here.