Programs are Proofs: Models and Types in Lambda Calculus

Lambda calculus started off with the simple, untyped lambda calculus that we've been talking about so far. But one of the great open questions about lambda calculus was: was it sound? Did it have a valid model?

Church found that it was easy to produce some strange and non-sensical expressions using the simple lambda calculus. In order to try to work around those problems, and end up with a consistent system, Church introduced the concept of *types*, producing the *simply typed lambda calculus*. Once types hit the scene, things really went wild; the type systems for lambda calculi have never stopped developing: people are still finding new things to do by extending the LC type system today! Most lambda calculus based programming languages are based on the Hindley-Milner lambda calculus, which is a simplification of one of the standard sophisticated typed lambda calculi called *SystemF*. There's even a [Lambda Cube][cube], though it's not related to the Time Cube. Once people really started to understand types, they realized that the *untyped* lambda calculus was really just a pathologically simple instance of the simply typed lambda calculus: a typed LC with only one base type.

The semantics of lambda calculus are easiest to talk about in a typed version. For now, I'll talk about the simplest typed LC, known as the *simply typed lambda calculus*. One of the really amazing things about this, which I'll show, is that a simply typed lambda calculus is completely semantically equivalent to an intuitionistic propositional logic: each type in the program is a proposition in the logic; each β reduction corresponds to an inference step; and each complete function corresponds to a proof! Look below the fold for how.

Types
---------

The main thing that typed lambda calculus adds to the mix is a concept called *base types*. In a typed lambda calculus, you have some universe of atomic values which you can manipulate; those values are partitioned into the *base types*. Base types are usually named by single lower-case greek letters: So, for example, we could have a type "σ", which consists of the set of natural numbers; a type "τ" which corresponds to boolean true/false values; and a type "γ" which corresponds to strings.

Once we have basic types, then we can talk about the type of a function. A function maps from a value of one type (the type of parameter) to a value of a second type (the type of the return value). For a function that takes a parameter of type "γ", and returns a value of type "δ", we write its type as "γ → δ". "→" is called the _function type constructor_; it associates to the right, so "γ → δ → ε" parses as "γ → (δ → ε)"

To apply types to the lambda calculus, we do a couple of things. First, we need a syntax update so that we can include type information in lambda terms. And second, we need to add a set of rules to show what it means for a typed program to be valid.

The syntax part is easy. We add a ":" to the notation; the colon has an expression or variable binding on its left, and a type specification on its right. It asserts that whatever is on the left side of the colon has the type specified on the right side. A few examples:

1. "*λ x : ν . x + 3*". This asserts that the parameter, *x*, has type "ν", which we'll use as the type name for the natural numbers. (In case it's hard to tell, that's a greek letter "nu" for natural.) There is no assertion of the type of the result of the function; but since we know that "+" is a function with type "ν → ν", which can infer that the result type of this function will be "ν".
2. "*(λ x . x + 3): ν → ν*". This is the same as the previous, but with the type declaration moved out, so that it asserts the type for the lambda expression as a whole. This time we can infer that "*x : ν*" because the function type is "ν → ν", which means that the function parameter has type "ν".
3. "*λ x : ν, y : δ . if y then x \* x else x*". A two parameter function; the first parameter is type "ν", the second type "δ". We can infer the return type, which is "ν". So the type of the full function is "ν → δ → ν". This may seem surprising at first; but remember that lambda calculus really works in terms of single parameter functions; multi-parameter functions are a shorthand for currying. So really, this function is: "*λ x : ν . (λ y : δ . if y then x \* x else x)*"; the inner lambda is type "δ → ν"; the outer lambda is type "ν → (δ → ν)".

To talk about whether a program is *valid* with respect to types (aka *well-typed*), we need to introduce a set of rules for type inference. When the type of an expression is inferred using one of these rules, we call that a *type judgement*. Type inference and judgements allow us to reason about types in a lambda expression; and if any part of an expression winds up with an inconsistent type judgement, then the expression is invalid. (When Church started doing typed LC, one of the motivations was to distinguish between values representing "atoms", and values representing "predicates"; he was trying to avoid the Godel-esque paradoxes, by using types to ensure that predicates couldn't operate on predicates.)

Type judgements are usually written in a sequent-based notation, which looks like a fraction where the numerator consists of statements that we know to be true; and the denominator is what we can infer from the numerator. In the numerator, we normally have statements using a *context*, which is a set of type judgements that we already know; it's usually written as an uppercase greek letter. If a type context includes the judgement that "*x : γ*", I'll write that as "Γ :- x : γ". However, it's really difficult to write fraction-form sequence in HTML, so instead, I'll write in a textual form.

1. The type identity rule
**Given:** nothing,
**Infer:** x:α :- x:α.
This is the simplest rule: if we have no other information except a declaration of the type of a variable, then we know that that variable has the type it was declared with.
2. The type invariance rul:
**Given:** Γ :- x:α, x != y,
**Infer:** Γ + y:β :- x:α.
This is a statement of non-interference. If we know that "x:α", then inferring a type judgement about any other term cannot change our type judgement for "x".
3. The function type inference rule
**Given:** Γ + x:α :- y:β,
**Infer: Γ :- (λ x:α . y):α→β.

This statement allows us to infer function types given parameter types. Ff we know the type of the parameter to a function is "α"; and we know that the type of the term that makes up the body of the function is "β", then we know that the type of the function is "α→β".
4. The function application inference rule
**Given:** Γ :- x:α→β, Γ :- y:α,
**Infer:** Γ :- (x y):β.

If we know that a function has type "α→β", and we apply it to a value of type "α", the result is an term of type "β".

These four rules are it. If we can take a lambda expression, and come up with a consistent set of type judgements for every term in the expression, then the expression is well-typed. If not, then the expression is invalid.

Just for kicks, here's how we'd write types for the SKI combinators. These are incomplete types - I'm using type variables, rather than specific types. In a real program using the combinators, you could work out the necessary substitutions of concrete types for the type variables. Don't worry, I'll show you an example to clarify that.

* **I combinator**: (λ x . x):α→α
* **K combinator**: (λ x:α.((λ y:β.x):β→&alpha)): α→β→α
* **S combinator:** (λ x : α → β → γ.(λ y : α→β.(λ z:α.(xz:β→γ (yz:β)))):(α→β→γ)→(α→β)→γ

Now, let's look at a simple lambda calculus expression: "λ x y.y x". Without any type declarations or parameters, we don't know the exact type. But we do know that "x" has some type; we'll call that "α"; and we know that "y" is a function that will be applied with "x" as a parameter, so it's got parameter type α, but its result type is unknown. So using type variables, we can say "x:α,y:α→β". We can figure out what "α" and "β" are by looking at a complete expression. So, let's work out the typing of it with x="3", and y="λ a:ν.a\*a". We'll assume that our type context already includes "*" as a function of type "ν→ν→ν", where ν is the type of natural numbers.

* "(λ x y . y x) 3 (λ a:ν . a \* a)": Since 3 is a literal integer, we know its type: 3:ν.
* By rule 4, we can infer that the type of the expression "a\*a" where "a:ν" is "ν", and \*:ν→ν→ν so therefore, by rule 3 the lambda expression has type "ν→ν". So with type labelling, our expression is now: "(λ x y . y x) (3:ν) (λ a:ν.(a\*a):ν) : ν→ν".
* So - now, we know that the parameter "x" of the first lambda must be "ν"; and "y" must be "ν→ν"; so by rule 4, we know that the type of the application expression "y x" must be "ν"; and then by rule 3, the lambda has type: "ν→(ν→ν)→ν.
* So, for this one, both α and β end up being "ν", the type of natural numbers.

So, now we have a simply typed lambda calculus. The reason that it's simply typed is because the type treatment here is minimal: the only way of building new types is through the unavoidable "->" constructor. Other typed lambda calculi include the ability to define *parametric types*, which are types expressed as functions ranging over types.

Programs are Proofs!
------------------------
Think about the types in the simple typed language calculus. Anything which can be formed from the following grammar is a lambda calculus type:

type ::= primitive | function | ( type )

primitive ::= α | β | δ | ...

function ::= type→type

The catch with that grammar is that you can create type expressions which, while they are valid type definitions, you can't write a single, complete, closed expression which will actually have that type. (A closed expression is one with no free variables.) When there is an expression that has a type, we say that the expression *inhabits* the type; and that the type is an inhabited type. If there is no expression that can inhabit a type, we say it's *uninhabitable*.

So what's the difference between inhabitable type, and an uninhabitable type?

The answer comes from something called the **Curry-Howard isomorphism**. For a typed lambda calculus, there is a corresponding [intuitionistic logic][intuitionistic]; a type expression is inhabitable if and only if the type is a *provable theorem* in the corresponding logic.

Look at the type "α→α". Now, instead of seeing "→" as the function type constructor, try looking at it as logical implication. "α implies α" is clearly a theorem of intuitionistic logic. So the type "α→α" is inhabitable.

Now, look at "α→β". That's *not* a theorem, unless there's some other context that proves it. As a function type, that's the type of a function which, without including any context of any kind, can take a parameter of type α, and return a value of a *different* type β. You can't do that - there's got to be some context which provides a value of type β - and to access the context, there's got to be something to allow the function to access its context: a free variable. Same thing in the logic and the lambda calculus: you need some kind of context to establish "α→β" as a theorem (in the logic) or as an inhabitable type (in the lambda calculus).

It gets better. If there is a function whose type is a theorem in the corresponding intuitionistic logic, then the program that has that type *is a proof* of the theorem. Each beta reduction is equivalent to an inference step in the logic.

The logic corresponding to the lambda calculus *is* it's model. In some sense, the lambda calculus and intuitionistic logic are just different reflections of the same thing.

[intuitionistic]: http://goodmath.blogspot.com/2006/05/logic-fun-intuitionistic-logic.html

More like this

This is one of the last posts in my series on category theory; and it's a two parter. What I'm going to do in these two posts is show the correspondence between lambda calculus and the [cartesian closed categories][ccc]. If you're not familiar with lambda calculus, you can take a look at my series…
I'm on vacation this week, so I'm recycling some posts that I thought were particularly interesting to give you something to read. ------------ In computer science, especially in the field of programming languages, we tend to use one particular calculus a lot: the Lambda calculus. Lambda calculus…
(This is a revised repost of an earlier part of my Haskell tutorial.) Haskell is a strongly typed language. In fact, the type system in Haskell is both stricter and more expressive than any type system I've seen for any non-functional language. The moment we get beyond writing trivial integer-…
Haskell is a strongly typed language. In fact, the type system in Haskell is both stricter and more expressive than any type system I've seen for any non-functional language. The moment we get beyond writing trivial integer-based functions, the type system inevitably becomes visible, so we need to…

Mark,
Is it possible to " translate " with the Curry-Howard isomorphism, a mathematical axiom system, say ZF or Peano, into a program ?
If yes, what's the result ? Is it an operating system, like Unix ?

Vicent, simply-typed lambda calculus does not have recursion, so it is actually not possible to program with it in the usual sense.
Axiomatization of arithmetics is possible, if not in simply-typed LC, at least in dependently-typed LC.

Vincent:

Before I answer your question directly, I think I should clarify something that I don't think was clear in the article.

The C-H isomorphism is a statement about *the type system*. Given a program, each beta step corresponds to an inference step in the logic; but it's an inference step *at the type level*. There isn't a one-to-one mapping between the evaluation steps of a specific lambda term and the proof steps of the corresponding logic: for an inhabitable type, there are multiple programs/terms with that type.

So you can't take a complete axiomatic system in a logic and translate it into a specific program in the calculus: there isn't a specific program.

In the simply typed lambda calculus you can't really do it at all: the type system isn't sufficiently powerful to allow a full recursive encoding. If you take things a step further, and go to something like SystemF, you have enough expressiveness to encode the formal system into the logic. What you get is pretty neat, but it's not what you think.

First - as I said above, the C-H isomorphism is over types - so the encoding of the axiomatic system isn't really quite what you would think; you get a meta-level thing: something that lets you analyze or manipulate LC programs. You can wind up with a couple of things: an interpreter; a type checker/inferencer; or a Chaitin-style axiomatic inference system.

Mark,
I think these is a mistake in the article.

"Each beta reduction is equivalent to an inference step in the logic."

Actually, the beta-reduction is corresponding to the proof normalization in logic rather than inference. The function application is corresponding to the inference.

Yu

"simply-typed lambda calculus does not have recursion, so it is actually not possible to program with it in the usual sense".

But lambda calculus is turing complete. Doesn't this mean that we can do anything with it, including writing parodies of Ernest Hemingway, and writing Lisp interpreters? Of course the effort required may not be practical. In fact, "turing complete" does not guarantee that the functions we write will run even in exponential number of lifetimes of the universe. Maybe this is what you mean.

cronos telfer
(Richard Mullins)

By richard mullins (not verified) on 27 May 2010 #permalink

"Mark, Is it possible to " translate " with the Curry-Howard isomorphism, a mathematical axiom system, say ZF or Peano, into a program ? If yes, what's the result ? Is it an operating system, like Unix ?
Posted by: Vincent | September 3, 2006 7:39 AM".

A professor of maths at UNSW (wildberger) is making similar suggestions. It sounds interesting, but I can't find his web pages now, they have disappeared.

By richard mullins (not verified) on 27 May 2010 #permalink