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).