7.3 Core calculus: Non-termination as a possible world
7.3.6 Full Zombie: Polymorphism and Datatypes
The core calculus includes the crucial rules dealing with termination-checking, in the context of a simpler language than full Zombie. Most of the additional features of Zombie do not interact with the termination-checking rules. The main place where
designing the Zombie type system required additional thought was deciding how the
Mobile judgement should interact with the rest of the typing rules.
Polymorphism Unlike this simplified core calculus, full Zombie allows polymorphic functions. In a dependent calculus like this, polymorphism is particularly easy to add. One generalizes the sort Type to a hierarchy Type0 :Type1 : Type2. . . (types, kinds, superkinds, . . . ), and generalizes the arrow rule to allow arrows from kinds as well as from types: Γ`L A:Type ` Mobile(A) Γ,x :L A`L B :Type `0 Γ`L(x:A)→B :Type max(`,`0)
So what about the mobile judgement? All the kinding rules in Zombie work atL; the only way to derive Γ`P A: Type
` is to check A at L and then use subsumption. So the sort Type` classifies exactly the same values inL and in P. Accordingly, the sorts are considered mobile: Mobile(Type`).
On the other hand, type variables (variables x:Type in the context) may eventually be instantiated to e.g. function types. Then they will classify a different set of values in L (terminating functions) and in P (any functions). So they are not considered mobile. Indeed, if theywere considered mobile, we could define an obviously unsound function that coerces any value from P toL:
log bogus : (a : Type) → (a@prog) → (a@log) bogus a x = x
(Here the body of the function would be checked by TUnboxVal, TMobileVal,
TBoxL.)
The face that type variables are not mobile is a little awkward, because of the require- ment that domains of arrow types must be mobile. So in the type of a polymorphic function like map, although the sort Type does not need an @-marker, the kinding rule forces us to mark not just function arguments, but also type variable arguments, with an explicit fragment:
map : [a b : Type] → (f : ((x:a@log) → b)@log) →
(xs : List a) → List b
In practice, it does not matter very much that we picked a@log instead a@prog. If we want, we can apply map to a list of programmatic values by giving it an argu- ment of type List (T@prog) and instantiating a with T@prog. Then the function being mapped should have type (T@prog@log) → b, which is essentially equivalent to(T@prog) → b.
In the Zombie implementation it is possible to hide some of the clutter by using a declarationusually logorusually progwhich causes the implementation to insert
a tag like @L whenever a non-mobile type occurs in a position where it is required to be mobile. For example, the type of composeP given in Section 7.2.2 is only valid if one implicitly inserts @prog for the function domains.
Datatypes The only datatypes in this small core calculus are natural numbers and dependent pairs. In prior work [31] we additionally treated sum types and iso- recursive types, the idea being that all inductive datatypes can be encoded using those features. In full Zombie, we instead treat datatypes as a primitive feature. Again, we should decide when a datatype is considered mobile. There are two options.
We could follow the style of Σ-types above, and compute whether a type is mobile or not based on whether its type arguments are. So for example, given a type definition
data List (a : Type) : Type where
Nil
Cons of (x : a) (xs : List a)
the type List T would be considered mobile exactly when T was mobile. Just like the second component of a Σ-type does not have to be mobile, there would be no requirement to tag the argument x to Cos with as P or L. On the other hand, this choice means that List a, where a is a type variable, would not be mobile. So the type of mapgiven above would not we well-formed, and the argument would need to be marked e.g. @log.
Instead, we choose to make all datatypes mobile, by requiring the arguments to data constructors to be mobile (like the first components of Σ-types above). So in Zombie, in order to make the datatype declaration well-formed we need to tag the constructor argument x@log:
data List (a : Type) : Type where
Nil
Cons of (x : a@log) (xs : List a)
In return, the type List a is now mobile. This decision is mostly a matter of taste, but we found that on balance this style of declaration creates less clutter in the types. The difference would be more striking if, in future work, we allowed non-logical types (Section 7.7.2 below).
Type-level computation Finally, full Zombie includes type level computation. This again highlights a limitation in the Mobile(A) judgement. In Zombie this is defined completely syntactically, just by examining the form of A. In the examples that can be expressed in the limited core calculus that works very well. Once one adds type definitions, however, it can start to feel restrictive.
For instance, consider defining a less-than relation on natural numbers, ltT m n. We could do this in at least two ways. Either as an inductive datatype (similar to the lt type in the Coq standard library), or as a type definition referring to a boolean comparison function:
ltT : Nat → Nat → Type
ltT = λ m n . (lt m n = True)
With the latter definition, ltT n m does not count as a mobile type, so it needs to be annotated with @s when used as a function argument type, even though it would be mobile if we inlined the definition. In other words, the Mobile(A) judgement does not respect β-equivalence.
One way to remedy this (in future work) would be to not check mobility syntactically, but instead track it in the type system. Maybe we should distinguish between sorts
Typeand MobileType? This would also provide a way to write polymorphic functions quantifying over mobile types only, and thereby not have to specify @θ on type variables.