Existential types

The implementation of a struct type can have _existentially bound type variables_ (as well as region variables, tag variables, and so on). Here is a useless example:

  struct T { <`a> `a f1; `a f2; };

Values of type struct T have two fields with the same (boxed) type, but there is no way to determine what the type is. Different values can use different types. To create such a value, expressions of any appropriate type suffice:

  struct T x = T{new 3, new 4};

Optionally, you can explicitly give the type being used for `a:

  struct T x = T{<int*@notnull> new 3, new 4};

As with other lists of type variables, multiple existentially bound types should be comma-separated.

Once a value of an existential variant is created, there is no way to determine the types at which it was used. For example, T(“hi”,”mom”) and T(8,3) both have type struct T.

The only way to read fields of a struct with existentially bound type variables is pattern matching. That is, the field-projection operators (. and ->) will not type-check. The pattern can give names to the correct number of type variables or have the type-checker generate names for omitted ones. Continuing our useless example, we can write:

  void f(struct T t) {
    let T{<`b> x,y} = t;
    x = y;
  }

We can now see why the example is useless; there is really nothing interesting that f can do with the fields of t. In other words, given T(“hi”,”mom”), no code will ever be able to use the strings “hi” or “mom”. In any case, the scope of the type `b is the same as the scope of the variables x and y. There is one more restriction: For subtle reasons, you cannot use a reference pattern (such as *x) for a field of a struct that has existentially bound type variables.

Useful examples invariably use function pointers. For a realistic library, see fn.cyc in the distribution. Here is a smaller (and sillier) example; see the following two sections for an explanation of why the regions(a) <=r stuff is necessary.

  int f1(int x, int y) { return x+y; }
  int f2(string x, int y) {printf("%s",x); return y; }
  struct T<`r::E> {<`a> : regions(`a) <= `r 
    `a f1; 
    int f(`a, int); 
  };
  void g(bool b) {
    struct T<`H> t;
    if(b)
      t = Foo(37,f1);
    else
      t = Foo("hi",f2);
    let T{<`b> arg,fun} = t;
    `b x = arg;
    int (*f)(`b,int) = fun;
    f(arg,19);
  }

We could replace the last three lines with fun(arg)—the compiler would figure out all the types for us. Similarly, the explicit types above are for sake of explanation; in practice, we tend to rely heavily on type inference when using these advanced typing constructs.