The Avail Programming Language

Type Parameterization

Some types depend on other values to specify their membership. While it is useful to talk about the type whose values are sets, it is typically more useful to talk about the type whose values are, say, sets of integers. Knowing that a set contains only integers, it is possible to specify operations that produce the sum of the members, find the minimum or maximum member, or ascertain whether each member is even. If it is only known that the instances of a type are sets, then it is unclear what operations may be performed with the instances; only the most basic operations that are meaningful for all sets are guaranteed valid.

A type that depends upon one or more other values is incomplete. Given an incomplete type A that depends upon a value B, it is said that B is a type parameter of A or, alternatively, that A is parametric on B. A type is considered complete if it either 1) does not have any type parameters or 2) has an actual value bound to each of its type parameters.

Consider the type set. In Avail, this type is incomplete. It has a type parameter, the element type, that restricts the membership of each instance to values of a particular type. The type set of {…}ᵀ is the most general completion, imposing no membership restrictions on its instances. The type set of {…, −1, 0, 1, …}ᵀ, on the other hand, has as instances only those sets whose members are exclusively integers.

It is clear to see that set of {…}ᵀ is a supertype of set of {…, -1, 0, 1, …}ᵀ. The former has all sets as its instances, and so trivially includes all sets whose members are integers. And in turn, set of {…, -1, 0, 1, …}ᵀ is a supertype of set of {1, 2, 3, …}ᵀ, whose instances are precisely the subsets of the set of strictly positive integers. Note that {1, 2, 3, …}ᵀ ⊆ {…, -1, 0, 1, …}ᵀ ⊆ {…}ᵀ and set of {1, 2, 3, …}ᵀ ⊆ set of {…, -1, 0, 1, …}ᵀ ⊆ set of {…}ᵀ. As the element type becomes more specific, the set type also becomes more specific: the complete type covaries with the type parameter.

Formally, given an incomplete type T, a complete type TX is covariant on type parameter X when, for two types A and B, A is a subtype of B implies that TA is a subtype of TB: T,A,B ∈ type: A ⊆ B → TA ⊆ TB.

Not every relationship between a complete type and a type parameter is covariant. Consider the following code example, which (incorrectly) treats a function type as covariant with respect to its parameter types.

/* "firstChar" can hold a function that accepts a single argument, which may be * anything at all, and answers a character. Assume that the assignment is valid * (but it isn't, and the compiler won't actually let us build this program). */ firstChar : [any]→character := [s : string | s[1]]; /* It's rather reasonable to ask for the first character of the string "zebra" … */ Assert: firstChar("zebra") = ¢z; /* … but what's the first character of the integer 15? This is not a meaningful * question, though the parameter type of "firstChar", which is "any", indicates * that any value should be appropriate here. Obviously something is wrong. */ Assert: firstChar(15) = …;

The actual function, [s : string | s[1]], requires that its argument be a string. Attempting to apply this function to something other than a string must therefore be illegal. But the variable firstChar holds a function that accepts any argument, so on its face firstChar(15) must be legal because 15 is an instance of any. And the application is, in fact, legal. So what is wrong here?

The problem is this: the initialization of firstChar is invalid. It implicitly reasons that because string is a subtype of any, then [string]→character must be a subtype of [any]→character. Though seductive, this reasoning is erroneous. Consider the following code snippet as a correction to the previous one:

/* "firstChar" can hold a function that accepts a single argument, which must be * any string other than the empty string "", and answers a character. Note that * "nonempty string" is a subtype of "string". */ firstChar : [nonempty string]→character := [s : string | s[1]]; /* It's still reasonable to ask for the first character of the string "zebra" … */ Assert: firstChar("zebra") = ¢z; /* … but now we are blocked from asking for the first character of the integer 15, * because 15 isn't a nonempty string … */ /* Assert: firstChar(15) = …; */ /* … and although the actual function would accept the empty string (and raise a * subscript-out-of-bounds exception), the parameter type of "firstChar" blocks * this usage. */ /* Assert: firstChar("") = …; */

The assignment is now correct. The actual function's parameter type may be more general than the one declared by firstChar because the actual function will only be applied to values of a type more specific than the one that it requires. As long as the parameter type of the actual function is no more specific than what firstChar specifies, then all applications of firstChar to instances of nonempty string must be correct. In fact, the parameter type of the actual function could be considerably more general; even if it were any, then all applications of firstChar would still need to conform to nonempty string, one of any's infinitely many subtypes. So as the parameter type of a function type becomes more general, the function type itself becomes more specific: the complete type contravaries with the type parameter.

Formally, given an incomplete type T, a complete type TX is contravariant on type parameter X when, for two types A and B, A is a subtype of B implies that TB is a subtype of TA: T,A,B ∈ type: A ⊆ B → TB ⊆ TA.

Avail provides no access to incomplete types. Parameterized types are constructed, ultimately, using primitive operations that accept the required type parameters and answer completed types.

‹ Type Compatibility | Return to Type System | Metatypes ›