The Indexed Model of Types: The following paper provides a nice, simple way to construct a model for recursive types (and recursive functions, impredicative polymorphism, subtyping, and a bunch of other features) that side-steps all of the problems we talked about last time, and makes it possible to construct logical relations directly on the terms: Andrew W. Appel and David McAllester. An Indexed Model of Recursive Types for Proof-Carrying Code. ACM TOPLAS, 23(5), Sept. 2001, pp 657-683. I'll try to summarize the key ideas in the paper here, but we have a local expert (Amal) who can tell you more. We're going to take as our term language: e ::= x | i | (e1,e2) | #1 e | #2 e | \x.e | e1 e2 | fix f(x).e v ::= i | (v1,v2) | \x.e with the usual reduction rules for call-by-value lambda calculus. In particular, the rule for reducing fix will be: fix f(x).e -> \x.e[fix f(x).e/f] The type system for the language is going to look like this: t ::= 'a | int | t1 x t2 | t1 -> t2 | mu (\'a.t) Here, 'a is a type variable and mu (\'a.t) represents a recursive type. For example the datatype D = D -> D can be encoded as mu (\'a.'a->'a) in this language. But don't think about this *syntactic* formulation of types. Rather, let's concentrate on a semantic interpretation first, and then come back and define the meaning of this syntax later on. What we're going to do is first define a notion of types-as-values, and as expected, these will be sets of term values. However, we're going to label each value with a natural number k. Defn: a VTYPE is a set S of pairs (k,v) such that k >= 0 and v is a value. Furthermore, if (k,v) in S, then for all j < k, (j,v) in S. In other words, for now, we're going to say that a VTYPE must be downward closed with respect to this index we've placed on values. Informally, the idea is that (k,v) can be in a VTYPE regardless of the value v, as long as a computation won't get stuck in k (or fewer) steps, running as if v were *actually* in the type. This notion will become clearer with the following definition: Defn: e :k T iff All jj e' and e' -/-> implies (k-j,e') in T. If you like, this is our computational notion of a type. But this definition is specific to a particular index k. It says that an expression e is in VTYPE T at level k if either e can take k steps without terminating, or else if it terminates in some number of steps less than k, then it must be a value. Furthermore, that value, coupled with the index k-j, must be in the VTYPE T. Note that for any VTYPE T and any expression e, we trivially have e :0 T since there is no j < 0. As another example, consider: e = pi_1 (pi_1 (3,4)) Let's take Top to be the VTYPE that includes all natural, value pairs. Then e :0 Top trivially as argued above, and e :1 Top (since e ->0 e and e is not irreducible.) However, it is not the case that e :2 Top since e ->1 pi_1 3 -/-> and pi_1 3 is not a value. So the definition of e :k T means intuitively that you can actually violate type-safety, but only after k steps. Of course, we're going to eventually define e : T to mean for all k >= 0, e :k T. That will ensure that we never get into a stuck state. Now we can start to define some VTYPE's: Bot = {} -- the empty VTYPE Top = { (k,v) } -- all values Int = { (k,i) } -- all integer values T1 x T2 = { (k,(v1,v2)) | All j < k.(j,v1) in T1 && (j,v2) in T2 } T1 -=-> T2 = { (k,\x.e) | All j < k.(j,v) in T1 implies e[v/x] :j T2 } The first three definitions are easy and it's easy to check that they are indeed downward closed so they are valid VTYPE's. For the product construction, it's also easy to see that if (k,(v1,v2)) in T1 x T2 then picking j < k, we have (j,v1) in T1 and (j,v2) in T2 (since they must be downward closed) so (j,(v1,v2)) in T1 x T2. How about the arrow construction? Suppose (k,\x.e) in T1 -> T2 and i < k. We must show (i,\x.e) in T1 -> T2. Pick a j < i and v s.t. (j,v) in T1. We need to show e[v/x] :j T2. Since (k,\x.e) in T1 -> T2 and j < i < k, we know that e[v/x] :j T2 and we're done. Before we get to the recursive types, let's establish the soundness of the usual syntactic proof rules. We begin by defining a type translation: T[int] = Int T[t1 * t2] = T[t1] x T[t2] T[t1->t2] = T[t1] -=-> T[t2] Next, we say that a substitution g mapping variables to terms satisfies G (a type assignment) to degree k if for all x in Dom(G), g(x) :k T[Gamma(x)]. When g satisfies G to degree k, we write g :k G. Defn [semantic soundness to degree k]: We write G |=k e : T to mean that for all g, g :k G => g(e) :k T. Defn [semantic soundness]: We write G |= e : T to mean that for all k, G |=k e : T. Let us now establish the semantic soundness of the usual typing proof rules: case: G |- x : G(x). Pick k. Pick a g s.t. g :k G. Then by assumption, g(x) :k G(x). case: G |- i : int. Pick k. Pick a g s.t. g :k G. Then g(i) = i and we need to show i :k Int. Since i cannot reduce, we need to show (k,i) in Int. But this follows from the definition of Int. G |- e1 : t1 G |- e2 : t2 case: ---------------------------- G |- (e1,e2) : t1*t2. Pick k and a g s.t. g :k G. We need to show g(e1,e2) = (g(e1),g(e2)) :k T[t1] x T[t2]. If g(e1) takes more than k steps to get to an irriducible value, then this holds trivially So suppose g(e1) terminates in j < k steps. By induction, we have g(e1) :k T[t1] which tells us that since g(e1) ->k e1', then e1' must be a value v1 such that (k-j,v1) in T[t1]. Now suppose that g(e2) takes more than k-j steps. Then the whole expression takes more k steps (i.e., (g(e1),g(e2)) ->j (v1,g(e2)) ->(k-j) (v1,e2') where e2 is not terminal.) So suppose g(e2) terminates in i < (k-j) steps. Then by induction, we know g(e2) ->i v2 for some v2 such that (k-j-i,v2) in T[t2]. We now need to show that (k-j-i,(v1,v2)) in T[t1] x T[t2]. Since k-j-i < k-j, we know that for all l < k-j-i, (l,v1) in T[t1] since T[t1] is a VTYPE (i.e., downward closed.) Furthermore, since T[t2] is a VTYPE, it follows that for all l, (l,v2) in T[t2]. Thus by the definition of T[t1] x T[t2], we have (k-j-i,(v1,v2)) in T[t1]xT[t2]. G |- e : t1*t2 case: -------------- G |- #1 e : t1 Pick k and g :k T[t1]. If g(e) takes more than k steps, then the result follows trivially. So suppose g(e) ->j e' such that e' is terminal and j < k. By induction, we have g(e) :k T[t1] x T[t2], so we know that g(e) ->j (v1,v2) for some (v1,v2) and that (k-j,(v1,v2)) in T[t1] x T[t2]. Thus, #1 g(e) ->j #1 (v1,v2) -> v1. Now since (k-j,(v1,v2)) in T[t1] x T[t2], it follows that for all i < k-j, (k-j,v1) in T[t1]. Thus in particular, (k-j-1,v1) in T[t1]. The case for #2 is similar. G,x:t1 |- e : t2 case: ------------------ G |- \x.e : t1->t2 Pick k and g such that g :k G. Since g(\x.e) = \x.g(e) is terminal, we must show (k,\x.g(e)) in T[t1->t2]. Pick a j < k and v s.t. (j,v) in T[t1]. We must show g(e)[v/x] :j T[t2]. Now it follows that since g :k G, that g :j G and furthermore, g[x->v] :j G,x:t1. Then by induction, we know that g[x->v](e) = g(e)[v/x] :j T[t2]. G |- e1 : t'->t G |- e2 : t' case: ------------------------------- G |- e1 e2 : t Pick k and then a g such that g :k G. We must show g(e1 e2) = g(e1) g(e2) :k T[t]. If g(e1) takes more than k steps then the result holds trivially. So suppose g(e1) ->j e1' where j < k. Then by induction, we know e1' is a value v1 such that (k-j,v1) in T[t'->t]. By the definition of T[t'->t], it follows that v1 is some \x.e. Now if g(e2) takes more than k-j steps, the result follows trivially. So suppose g(e2) ->i e2' for some i < k-j. Again, by induction, we know that e2' is some value v2 such that (k-j-i,v2) in T[t']. We must now argue that (\x.e) v2 :k-j-i T[t] or equivalently, that e[v2/x] : k-j-i-1 T[t]. Since (k-j-i,v2) in T[t'] and T[t'] is downward closed, we know (k-j-i-1,v2) in T[t']. Since (k-j,\x.e) in T[t'->t], and k-j-i-1 < k-j we have from the definition of T[t'->t] that e[v/x] : k-j-i-1 T[t]. G,f:t1->t2 |- \x.e : t1->t2 case: --------------------------- G |- fix f(x).e : t1->t2 Here, we'll argue by induction on k. base case [k=0]: Pick g s.t. g :0 G. We must show g(fix f(x).e) :0 T[t1->t2] but this holds trivially since fix is not a value. inductive case: Assume the lemma holds for all i < k. Pick a g s.t. g :k G. We must show g(fix f(x).e) :k T[t1->t2]. Now g(fix f(x).e) = fix f(x).g(e) -> \x.g(e)[fix f(x).g(e)/f] so it suffices to show \x.g(e)[fix f(x).g(e)/f] :k-1 T[t1->t2]. From the (outer) induction hypothesis, it's sufficient to show that g[f->fix f(x).g(e)] :k-1 G,f:t1->t2. For this, it suffices to show that fix f(x).g(e) :k-1 T[t1->t2]. But this follows from the inner induction on k. Okay, so this last case really shows how crucial the index is to our argument. If it hadn't gone down, then we'd have been in the same trouble I mentioned earlier --- the outer induction hypothesis would not apply. What about recursive types? The typing rules for these look like this: G |- e : mu F G |- e : F(mu F) ---------------- ---------------- G |- e : F(mu F) G |- e : mu F In essence, these rules allow us to equate the two types mu F and F(mu F). Notice that if we have these rules, then we don't need recursive functions: We can code up the CBV fixed-point combinator (at a given type): fix = \f.(\x.f(\y.x x y))(\x.f(\y.x x y)) : ((a->b)->(a->b)) -> a -> b The trick to typing this is to assign x the type: mu (\d.d -> a -> b) Or to see it another way, in ML, we can write: datatype ('a,'b) Fix = Fix of ('a,'b) Fix -> 'a -> 'b and define: val fix : (('a -> 'b) -> ('a -> 'b)) -> 'a -> 'b = fn (f : ('a -> 'b) -> ('a -> 'b)) => (fn (Fix x) => f (fn z => x (Fix x) z)) (Fix (fn (Fix x) => f (fn z => x (Fix x) z))) Then it becomes possible to write things like: val fact = fix (fn f => fn i => if i <= 0 then 1 else i * (f(i-1))) Instead of concentrating on the syntactic form of mu F, let's concentrate on the semantic form of the function F. We would like to impose some conditions on the function F to ensure that we get a VTYPE (ie, something downward closed.) Furthermore, we would like to impose some conditions on F so that we'll be able to use its finite unwindings to prove properties about the limit. To that end, we define: Mu F = { (k,v) | (k,v) in F^(k+1)(Bot) } That is, if F is a functional mapping VTYPEs to VTYPEs, then we'll define Mu F to be as above. Now it helps to define the notion of an approximation of a type as follows: Defn: approx(k,T) = {(j,v) in T | j < k} Note that if T is a VTYPE, then approx(k,T) is a VTYPE. Note also that approx(0,T) = Bot = {}. Intuitively, we want Mu F to be a VTYPE such that we can prove e :k mu F by simply showing that for all j < k, e :j approx(j,mu F). Defn: F : VTYPE->VTYPE is well-founded if for any VTYPE T and k >= 0: approx(k+1,F(T)) = approx(k+1,F(approx(k,T))) It turns out that the proof rules above for recursive types are valid when F is a well-founded function from VTYPEs to VTYPEs. Of course, we need to establish this (see below). But the intuition is that when we have Mu(\T.F(T)) then we can think of the recursive definition: T = F(T) as being well-founded if T gets smaller each time we go around the loop. Consider a simple F such as F(T) = T -==-> Bot. The only values in here are going to be of the form \x.e at some index k. But we only have to apply this function to values with a smaller index, so we can prune out all of those values in Mu F that have an index equal to or greater than k. The definition above provides sufficient conditions to ensure that unrolling F makes the set smaller. As another example, consider F(T) = F(T). In this case, there's no chance for the set to get smaller, so F = \T.T (the identity) is *not* well-founded. (But it turns out that any expression e with type mu(\a.a) must diverge!) Lemma 1: For any well-founded F, T1, and T2 approx(j,F^j(T1)) = approx(j,F^j(T2)) Proof: by induction on j. base case: approx(0,F^0(T1)) = approx(0,F^0(T2)) inductive case: approx(i+1,F^(i+1)(T1)) = approx(i+1,F(F^i(T1))) = approx(i+1,F(approx(i,T1))) = approx(i+1,F(F^i(T2))) = approx(i+1,F^(i+1)(T2)) Corrollary 2: For any well-founded F, j <= k and T1 approx(j,F^j(T1)) = approx(j,F^k(T1)) Proof: use the above lemma taking T2 = F^(k-j)(T). What these lemmas tell us is that if F is well-founded and we only consider up to some approximation, then it doesn't matter what VTYPE we apply it to. Theorem 3: If F is well-founded then Mu F is a VTYPE. That is, it's closed under decreasing index. Suppose (k,v) in mu F and let j <= k. Then: (k,v) in F^(k+1)(Bot) defn. of Mu F (j,v) in F^(k+1)(Bot) defn. of a VTYPE (j,v) in approx(j+1,F^(k+1)(Bot)) defn. of approx (j,v) in approx(j+1,F^(j+1)(Bot)) by above corrollary (j,v) in F^(j+1)(Bot) defn. of approx (j,v) in Mu F defn. of Mu F. Lemma 4: if F is well-founded then: (a) approx(k,Mu F) = approx(k, F^k(Bot)) (b) approx(k+1,F(Mu F)) = approx(k+1,F^(k+1)(Bot)) [see paper or work out as an exercise]. Lemma 5: if F is well-founded approx(k, Mu F) = approx(k, F(Mu F)) approx(k,Mu F) = by above lemma 4(a) approx(k,F^k(Bot)) = by corollary 2 approx(k,F^(k+1)(Bot)) = (approx(k,approx(k+1,T)) = approx(k,T)) approx(k,approx(k+1,F^(k+1)(Bot))) = by above lemma 4(b) approx(k,F(Mu F)) (approx(k,approx(k+1,T)) = approx(k,T)) Theorem 6: if F well-founded then Mu F = F(Mu F). Proof: We have that (k,v) in Mu F iff (k,v) in approx(k+1,mu F) iff (k,v) in approx(k+1,F(Mu F)) iff (k,v) in F(Mu F). So this theorem justifies the two proof rules as long as we can show that F is well-founded. Defn: a non-expansive type constructor F is one such that approx(k,F(T)) = approx(k,F(approx(k,T))) For example, the constructor \T.T is nonexpansive but as we argued above, not well-founded. Lemma (non-expansive constructors): a. every well-founded constructor is non-expansive b. \a.a is non-expansive c. \a.T where a is not free in T is well founded. d. The composition of non-expansive constructors is non-expansive. e. The composition of a non-expansive constructor with a well-founded constructor (in either order) is well-founded. f. If F and G are non-expansive, then \a.(F a) -=-> (G a) is well-founded. g. if F and G are non-expansive, then \a.(F a) x (G a) is well-founded. We need one more lemma: Taking F = \T.T then mu F = F(Mu F) which is clear. >From this, it follows that any constructor F = \a.T built from a, int, Top, Bot, x, or -=-> is either well-founded or the identity. And thus, for any well-formed type, we have mu F = F(mu F).