Skip to content
wangp edited this page Sep 24, 2017 · 1 revision

We know we can define a type for lists or other containers of a single type like this:

:- type list(T)
    --->    []
    ;       [T | list(T)].

As usual, type variables that occur in the body of the type definition must occur as arguments on the left side of the arrow. Such variables are universally quantified, meaning you can substitute any type for each of the variables to get a new type.

By design, all elements of a list(T) term must have the same type, but what if you want a list in which the elements may have different types? Rather than substituting for a single type variable, you would need a distinct type variable for each element of the list. We can define a suitable type using existentially quantified type variables (or "existential types"):

:- type heterogeneous_list
    --->    nil
    ;       some [T] cons(T, heterogeneous_list).

The data constructors are named nil and cons just to distinguish them from the usual lists.

The some VARS syntax before the data constructor introduces a list of existentially quantified type variables. The definition of heterogeneous_list says that each cons/2 constructor has a distinct type variable T; the type variable is hidden within the data constructor. Notice there is also no longer a universally quantified type variable on the left side of the type definition.

Construction

For reasons we won't get into, to construct a term of an existentially quantified functor, you must prepend 'new ' to the functor name. For example:

List = 'new cons'(kitty, 'new cons'(doggy, nil))

Deconstruction

Deconstructing terms with existential types requires nothing special. We could switch on the previously constructed List just as normal:

(
    List = nil
;
    List = cons(Head, _Tail),
    ...
)

The thing that is a bit tricky is the type of Head. The actual type of Head could be anything, depending on what List is bound to when that code is executed. All we know is that Head has some type, which is represented in Mercury as some [T] T.

Tip: sometimes you will get error messages from the compiler to the effect of "expected some [T] T but got some [T] T". Don't be confused: those are two distinct type variables, and the compiler is saying that they might be bound to two different types.

Existentially typed predicates and functions

The head/2 predicate for heterogeneous_list looks like this:

:- some [T] pred head(heterogeneous_list, T).
:- mode head(in, out) is semidet.

head(cons(Head, _Tail), Head).

That's new. The type variable of the return argument had to be existentially quantified. If the type variable were universally quantified (the default behaviour), that means the caller should be able to call head/2 with any type substituted for T. But we know that the type of the return argument actually depends on what head/2 finds as the first argument of the heterogeneous list.

Run-time type checking

What can you do with a term if you don't know its concrete type? For one, you will have the type at run time so you can do things that only make use of the run-time type information, such as print it out with io.write.

In most cases, you will probably want to perform a dynamic type check to see if the term has a type that you were expecting. This can be done with the builtin predicate dynamic_cast/2:

    % dynamic_cast(X, Y) succeeds with Y = X iff X has the same ground type
    % as Y (so this may succeed if Y is of type list(int), say, but not if
    % Y is of type list(T)).
    %
:- pred dynamic_cast(T1::in, T2::out) is semidet.

Here is an example of its use:

( if
    List = cons(Head, _Tail),
    dynamic_cast(Head, Cat)
then
    meow(Cat, !IO)
else
    write_string("The first element is not a cat.\n", !IO)
)

The univ type

The standard library provides a "universal type" in the univ module that can hold a term of any type:

    % An object of type `univ' can hold the type and value of an object of any
    % other type.
    %
:- type univ.

No surprises, internally it's a wrapper for an existential type.

:- type univ
    --->    some [T] univ_cons(T).

The univ type is commonly used in the standard library when a universal type is required. For example, any ground term can be thrown as an exception value so when an exception is caught, the exception value is bound to a variable of type univ.

Run-time representation

You might wonder how data structures containing existential types are represented. A data constructor requires an extra memory word for each existentially quantified type variable that it contains. The extra word is used to store a pointer to a run-time representation of the type bound to that type variable, called a "type info".

So a univ term requires two words for the univ_cons/2 constructor. The first word holds the type of the value inside the univ, and the second word holds the value itself.

When you call dynamic_cast/2 it compares that the two type infos of its input arguments represent the same type, otherwise it fails.

Sample code

:- module exist_types.
:- interface.

:- import_module io.

:- pred main(io::di, io::uo) is det.

:- implementation.

:- type heterogeneous_list
    --->    nil
    ;       some [T] cons(T, heterogeneous_list).

:- type dog
    --->    fido
    ;       rex.

:- type cat
    --->    kitty
    ;       tiger.

:- type rock
    --->    rocky.

main(!IO) :-
    List = 'new cons'(tiger, 'new cons'(fido, 'new cons'(rocky, nil))),
    print_list(List, !IO).

:- pred print_list(heterogeneous_list::in, io::di, io::uo) is det.

print_list(List, !IO) :-
    ( if head_tail(List, Head, Tail) then
        ( if dynamic_cast(Head, Dog) then
            woof(Dog, !IO)
        else if dynamic_cast(Head, Cat) then
            meow(Cat, !IO)
        else
            write_string("Not a dog or cat!\n", !IO)
        ),
        print_list(Tail, !IO)
    else
        write_string("The end.\n", !IO)
    ).

:- some [T] pred head_tail(heterogeneous_list, T, heterogeneous_list).
:- mode head_tail(in, out, out) is semidet.

head_tail(cons(Head, Tail), Head, Tail).

:- pred meow(cat::in, io::di, io::uo) is det.

meow(Cat, !IO) :-
    (
        Cat = kitty,
        Name = "Kitty"
    ;
        Cat = tiger,
        Name = "Tiger"
    ),
    write_string(Name, !IO),
    write_string(" says meow!\n", !IO).

:- pred woof(dog::in, io::di, io::uo) is det.

woof(Dog, !IO) :-
    (
        Dog = fido,
        Name = "Fido"
    ;
        Dog = rex,
        Name = "Rex"
    ),
    write_string(Name, !IO),
    write_string(" says woof!\n", !IO).