Typed lambda calculus
January 11, 2014 (Saturday) reddit thisIn memory of Sanjeev Kumar Aggarwal (ska).
Update: An implementation of propositional logic in agada is available on my github repository sample-code/agda/Logic.agda
In this post, I briefly introduce typed lambda calculus in its simplest form and explain the type inference rules. The Curry-Howard isomorphism appears for the first time here, although only for propositional logic.
What are types ?
We fix a set of base types that can be thought of as the built-in types of the language. For this post, the set of types are all terms generated inductively as follows:
The above inductive definition means that a type is either a basic type , i.e. an element of the set , or it is the function type where and are themselves types. The type captures the type of functions whose domain is of type and range is of type . There are few points that we want to clarify right away.
Consider the set of propositional logic formulas where the base set of propositions is and the only logical connective is the logical implies . There is a one-to-one correspondence with types of our language: interpret the functional type symbol as the implication operator . We will see in this post that this connection is not just skin deep.
The types in our language are defined independent of the terms in our lambda calculus. While this is possible for the version of typed -calculus that we define here, when we want more powerful types this will no more be possible. We will have to define the values and types together.
What is a well typed expression ?
Intuitively, a typed lambda calculus is a version of lambda calculus where each expression is assigned a type. This type is used to ensure that function application is of the correct type, i.e. whenever an expression of type is applied on another expression of type , the type should be a function type , for type . This requires us to assign types to lambda calculus terms in a systematic way. We use the notation to assert that the expression has type . Recall that a -calculus term is a variable, or an application or a -abstraction. Therefore, we need to give systematic rules for assigning types for these three cases.
The base case would be to assign types to variables in an expression. We ensure that the -abstraction also have to assert the type of the variable it quantifies. Thus, any -abstraction is of the form and the bound variables gets its type from this abstractions, i.e. every free occurrence of in is assumed to be having the type . For the free variables, the only way we can have types is by type assumptions, a set of assertions of the kind . The type of an expression thus depends on the type of the free variables in it. Therefore, a well typed lambda calculus expression should be a combination of an expression and a set of type assumption for the free variables in it. Furthermore, they should satisfy some additional rules which we call the type inference rules.
A well typed -calculus expression is an ordered pair of (1) a type assumption together with (2) a typed -calculus term , written in the logical style , subject to the following inference rules:
Variable Rule (VAR) :
Rule for application (APP) :
Rule for -abstraction (ABS) :
The notation that we used for defining well typed terms is like a set of logical deduction rules. The stuff on the top of the horizontal line are pre-conditions, i.e. stuff which we already have derived, and the stuff in the bottom are consequences. i.e. stuff that can be concluded given the pre-conditions on the top. Let us now interpret each of these rules informally:
VAR : This rule states that with no pre-conditions we can derive the type assertion provided it is already an assumption. This rule takes care of the free variables in the expression.
APP : This is the rules that makes sure that the functions are applied to the right arguments. It makes sure of two things (1) We are allowed to form the expression under the type assumption if and only if and have types and respectively under the assumption and (2) then the resulting expression has type under .
ABS : This rules assign types to the bound variables. This rule needs a bit of getting used to as we normally think in the other direction, i.e. in the expression , the occurrence of in has type .
In all the above rules, we assume that the type assumptions that arise on the left hand side of the symbol satisfies some obvious conditions like, no variables should have two distinct type assumptions etc.
Curry-Howard isomorphism.
Recall that any type can be seen as a proposition over the set of basic proportion by interpreting the function type operator as the Boolean implies operator . Consider the three type inference rules that we discussed and replace any type assumption or type assertion in the rules above with just the associated proposition and respectively. This gives a set of inference rules for a restricted form of propositional logic that has implies operator as the only Boolean operator. This isomorphism between type inference and logical inference rules is often called the Curry-Howard isomorphism and is one of the main ideas of type theory. The goal in the rest of the post is to give a different interpretation of typed lambda calculus so that some magic is removed out of this isomorphism.
Consider the type assertion . Normally, we think of types as a subset of allowed values and the assertion as saying that is a value in the set associated with . There is an alternate interpretation which makes the logical connection easy to make. Think of as a logical statement, proposition in this case. An assertion of the form is thought of as being the proof of the statement . Alternatively, we think of the type as the set of all proofs of the proposition associated with in which case our usual interpretation means that means that is a proof of the proposition . In this interpretation, a proof of the proposition should be treated a method to convert proofs of to proofs of . This then gives a valid interpretation of all the rules of type inference as rules of building proofs.
VAR : We can create a proof of if we have such a proof by one of the assumption.
APP : If is a proof of then it is a function that converts proofs of to . If in addition, we have a proof of , we can apply to it and get the proof of .
ABS : If assuming a proof of we were able to get a proof (which can make use of as an axiom) of then the function is a proof of as it takes any input proof of and produces as output the proof of . Here denotes replacing all free occurrence of by .
This essentially is the crux of the “types as proofs” view point. If we want to effectively use this logical content of type theory, we need to look at richer and richer types and that is what we would be doing. We make some modest enriching in this post and leave the rest for future posts.
Introduction/Elimination rules.
For the logical operator , the ABS rule serves as the introduction rule as its post-condition is an implication. Any proof of a formula that is essentially an implication therefore would have ABS as its last step. On the other hand APP serves the dual purpose. It is the elimination rule for the operator . You may think of VAR rules as both an introduction as well as an elimination rule (it introduces nothing and eliminates nothing). This style of presentation of logical rules is due to Gentzen and plays an important role in designing type theoretic systems. The introduction rules gives ways to “construct” objects of a given type and elimination rules gives ways to “use” the objects in expressions. All other operators that we introduce here will also have introduction and elimination rules.
The operators and .
We would like our logic to have the conjunctions and disjunctions. At the type level we just need to add two additional type formation operators and . As a result our types are now inductively defined as:
To prove conjunctions and disjunctions, we need ways to create values (remember they are our proofs) of type and respectively. For conjunctions, we introduce the primitive that pairs up two expressions to give a new expression. A value of type can be created in two ways: by applying the constructor on a value of type or by applying the constructor on a value of type . These give the required introduction rules for the logical operators and :
DISJ :
CONJ :
The justification of these rules from a proof theoretic point of view is that one can give a proof of by giving a pair of proofs where the first component is a proof of and the second a proof of . Similarly, we give a proof of by giving either a proof of or except that we need to explicitly state which is the case by using the appropriate constructor or .
Next we give the elimination rule for . The corresponding language primitive that we need is the projection operators which we add as built in functions in our calculus.
PROJ :
Similarly, to obtain the elimination rules for , we add the function that does case by case analysis.
CASE :
We define the the type of logical equivalence of and as .
Truth, falsity and negation.
We have a proof theoretic view of truth and falsity in this setting. Propositions are “true” if they can be proved by giving a -calculus expression of that type and not true otherwise. In that sense, we only have “truths” and “not truths” and every inhabited types, i.e. types for which we can construct an element with that type, is true. Explicit truth and falsity can be achieved by adding types and to the basic types and to make provable, we enrich the -calculus with a single constant of type . That there is no constants with type is deliberate design choice as we do not want to prove in our logic. Once we have the type , we can define the negation of to be the type .
The law of excluded middle, LEM for short, is the statement and is not a basic axiom of our logic. We can in fact prove one direction : Consider the function . It is easy to see that we can assign to it the type for any types and . In particular, if is the type , we have . The converse however is not provable. This might be considered as a weakness of the logic because LEM is used through out mathematics. However, there is a distinct advantage here.
The proofs that we build are constructive and
If we are in dire need of the opium called LEM, we can recover it by adding a constant of type the way we added the constant . However, this is never done in practice when using a proof assistant like
coq
oragda
Comparison to untyped -calculus.
For any typed -calculus term we can get an untyped -calculus term, denoted by itself, by ``erasing’’ out all types from abstractions. Is it possible to do the other way? Can we assign types to an untyped -calculus expression in a way that is consistent to the rules defined above? Doing this algorithmically is the type inference problem. For example, consider the expression . Intuition tells us that this can be assigned any type as it is the identity function. Indeed this is the case:
by VAR
by ABS and (1).
It turns out that not all terms can be consistently typed, for example cannot be assigned any type (why?).
Recall that the computational content of untyped -calculus is captured in the notion of -reduction. To avoid variable collision we also need -conversions. It is easy to see that notion of -conversion and -reduction can be defined in a straight forward way for typed -calculus. What then is the difference? The typed version of -reduction is strongly normalising. It turns out that term like and fixed point combinators cannot be consistently typed. As a result general recursion, and hence infinite loops, are not possible in this calculus.
Consistency and recursion
The Curry-Howard isomorphism gives us a way to define logical system out of typed lambda calculus. Enriching the basic typed lambda calculus with constants like or is like adding axioms to the logical system assoicated with the language. In any logical system, we need to worry about consistency. In classical logic, a set of axioms together with the inference rules form an inconsistent system if one can prove a statement and its negation . This definition is not very useful in the type theoretic setting as it is crucially dependent on negation which we want to avoid as much as possible. An alternate way to define inconsistency is to define inconsistent system as those which proves all statements. It is this definition of inconsistency that is easier to work with in the type theoretic framework. We say that a type theoretic system, i.e. the under lying typed -calculus and its type inference rules, is inconsistent if every type in inhabited. This makes sense because an inhabitant of a type is the proof of the statement (associated) to . In this section, we want to connect consistency and the ability to do recursion. In fact, arbitrary recursion, or equivalently a uniform way to compute fixed points, is dangerous from a consistency perspective.
We saw that the typed -calculus that we defined does not support fixed point combinators and therefore does not support recursion. This severely limits the kind of programs that one can write in such a language. However, we do know that fixed points can be implemented on a computer. Can we enrich the -calculus to include a fixed point combinator by force? After all, we do know how to compile it into machine code. What would happen if we just enrich the calculus with a constant , much like the way we included a constant or . For this to workout, we would need to augment our type inference rules with the following rule for
FIX :
This would mean that, if we some how create a function of type then we can prove using the proof . Recall that, for any type , the typed lambda calculus expression has type . Taking its fixed point will give an inhabitant of . Therefore, adding arbitrary fixed points will make the logic inconsistent.
Real world programming languages like Haskell does not care about this issue as writing infinite loops are too important for a programmer. In fact, every type in Haskell has an inhabitant, namely undefined
. What this means is that the type system of Haskell is not directly suitable as a theorem prover although we can still use it to catch many bugs at compile time.
Languages like agda which has to double up as a proof assistant allow certain restricted kinds of recursion by making sure that the recursion is well formed. Other than the motivation to write real world programs, some restricted form of recursion is actually necessary to capture mathematical objects like natural numbers etc. We leave these issues for future posts.