Intro to the Simply-Typed Lambda Calculus In the pure lambda calculus, the only values we have are functions. We can encode just about anything with functions, but the encodings aren't very practical. In real languages, such as ML or Java, we have a set of built in constants and operations, and perhaps built in data structures such as record/object/tuples/etc. So, let's consider what happens if we add a few built-in things to the lambda calculus: e in Exp ::= x | \x.e | e1 e2 | i | e1 + e2 To keep things simple for now, I've only added integers (i) and addition (e1 + e2). So, our values consist of functions and integers: v in Value ::= \x.e | i Note that every value is an expression, so values are a subtype of expressions. We need to update our operational semantics. The (call-by-value) small-step semantics for this language might look the following: (\x.e1) v => e1[v/x] e1 => e1' --------------- e1 e2 => e1' e2 e2 => e2' ---------------------- (\x.e) e2 => (\x.e) e2' i1 + i2 => i (i = i1 + i2) e1 => e1' ------------------- e1 + e2 => e1' + e2 e2 => e2' ----------------- i + e2 => i + e2' The large-step semantics would look like this: i evalsto i \x.e evalsto \x.e e1 evalsto \x.e e2 evalsto v2 e[v2/x] evalsto v --------------------------------------------------- e1 e2 evalsto v e1 evalsto i1 e2 evalsto i2 ----------------------------- (i = i1 + i2) e1 + e2 evalsto i All looks good, except that there's a problem. What happens if we attempt to evaluate the expression: (\x.\y.x + y) 3 (\z.z) => (\y.3 + y) (\z.z) => 3 + (\z.z) Now we're stuck. The problem is that we're trying to add a function (\z.z) to an integer. But the primitive + is really only defined on integers. There are two ways we can try to fix this problem: (1) make + defined on all possible values, (2) try to rule out these kinds of type errors before running the program. For example, we could just add an axiom: v1 + v2 => 0 (v1 or v2 not an integer) Of course, that's not the only problem. We can also get stuck in a situation where we attempt to "call" an integer: (\x.\y.y(x)) 3 42 => (\y.y(3)) 42 => 42(3) Again, we could just add an axiom: v1 v2 => 0 (v1 not a function). Notice that an implementor of the language has to be able to tell functions from integers at run-time and detect these bad situations. In other words, at run-time, we will need type tags and type tests to implement this semantics. This is effectively what Scheme and other dynamically typed languages do. However, instead of returning 0, they usually return some distinguished error value: v1 + v2 => error (v1 or v2 not an integer) v1 v2 => error (v1 not a function) Furthermore, they propagate the error: (\x.e) error => error So, if you evaluate a sub-expression and get an error, you'll get an error for the whole expression. Just like, if you evaluate a sub-expression and it doesn't terminate, then the whole expression doesn't terminate. An alternative is to try to rule out such type-errors at compile time by doing an analysis, similar to what you did for IMP. In the little language above, we need to be able to distinguish integer values from functions so that we won't get stuck with + or application. But we need a bit more than that -- we also need to know what kinds of arguments a function might take, and what kind of results it yields in order to ensure that we never get stuck. This gives rise to the following simple type structure: t in Type ::= int | t1 -> t2 Now, we're going to construct a syntax-directed analysis of program to figure out what the type of each expression is. We'll rule out programs of the form e1 + e2 when e1 and e2 might evaluate to non-integers, and we'll rule out expressions of the form e1 e2 when e1 might evaluate to a non-function. To do this, we'll use a relation of the form: G |- e : t Here, G is a type environment (aka symbol table) and is a partial function from variables to types: G in TyEnv : Var -partial-> Type The proof rules are as follows: G |- i : int G |- x : G(x) -- note that G must be defined on x to use this rule G |- e1 : int G |- e2 : int ----------------------------- G |- e1 + e2 : int G[x -> t1] |- e : t2 --------------------- G |- \x.e : t1 -> t2 G |- e1 : t'->t G |- e2 : t' ------------------------------- G |- e1 e2 : t We say a program e is well-typed if {} |- e : t, usually just written |- e : t. That is, if under an empty set of typing assumptions, we can prove that e has type t, then e is well-typed. Theorem: If |- e : t, then either e is a value v or else there exists an e' such that e => e' and |- e' : t. What does this theorem say? It says that if we have a well-typed expression, then it's either a value (an answer) so there's no step to take, or else it can step to some e' and e' will be well-typed. More importantly, let us write down all the possible terminal states (i.e., states for which there is no small step to take) and classify them as "good" (i.e., answers) or "stuck" (i.e., a type error of some sort): Good terminal states: i -- okay, a value \x.e -- okay, a value Bad terminal states: x -- a free variable i v -- application of an integer (\x.e) + v -- addition of a function on left v + (\x.e) -- addition of a function on right e1 e2 -- where e1 is stuck v e2 -- where e2 is stuck e1 + e2 -- where e1 is stuck v + e2 -- where e2 is stuck A corollary of the theorem above is that if |- e : t, then there is no e' such that e =>* e' and e' is a bad terminal state. In other words, well-typed expressions cannot get stuck. We either evaluate to a value or run forever, but we can't get into one of these stuck states. If this theorem is true, then an implementor does not need type-tags or type-tests at run-time. They've proven that well-typed programs can't get stuck at say, an addition of a function. So they don't have to check for this bad case at run-time. The theorem above is usually called "Type Soundness". It's really saying that our little analysis is conservative -- it will never say something is well-typed that might end up with a run-time type error. How do we prove the theorem? We can break it into two pieces: (a) show that |- e : t is invariant under evaluation. That is, if |- e : t and e => e', then |- e' : t. (b) show that if |- e : t, then either e is a value (i.e., good terminal state) or else there exists an e' such that e => e'. Lemma (a) is called Preservation (aka Subject Reduction) and lemma (b) is called Progress. Basically, we're showing that well-typedness is a "loop invariant" for the meta-level interpreter of the language.