The Calculus of Constructions
Abstract
This post is part of a series on how to reason about code. It describes \(\lambda C\), a precursor to the formal language understood by Coq, an interactive proof assistant I'm learning. Prior posts:
- first, long-ago post here
- a concrete example of symbolic execution
- why I'm interested in type theory
- the λ Calculus
- the Simply Typed Lambda Calculus
- \(\lambda 2\)
- \(\lambda\underline{\omega}\)
- \(\lambda P\)
This exposition follows that in "Type Theory & Formal Proof" by Nederpelt & Geuvers (abbreviated TTFP throughout), with help from "Lambda Calculi with Types" by Henk Barendregt. Also recommended is the post A Guide to Typed Lambda Calculus.
Recap
In this series, we've introduced the λ Calculus as a formal model of general computation & we've added a type system (\(\lambda\rightarrow\)) to regularize the terms therein. \(\lambda\rightarrow\) provided a strong normalization guarantee but was not very expressive. In truth, it had a number of other desirable properties, as well, but I never got into those. We've spent the past few posts exploring extensions to \(\lambda\rightarrow\) that preserved those nice features while giving more expressiveness:
This post will bring those extensions together into a unified family of typed lambda calculii.
The λ Cube
In some sense, \(\lambda 2\), \(\lambda \underline{\omega}\) and \(\lambda P\) are "independent" of one another. If \(\lambda\rightarrow\) provides a system whose terms depend upon other terms…
- \(\lambda 2\) provides terms depending on types, in that \(\Lambda\alpha:\ast.\lambda x:\alpha.x\) can only be used for computation after being applied to a type
- \(\lambda \underline{\omega}\) provides types depending on types, in that we can here build functions on types, such as \(\lambda\alpha:\ast.\alpha\rightarrow\alpha\)
- \(\lambda P\) provides types depending on terms in the form of dependent types
Therefore, they can be combined at will: we can imagine a type system that provides terms depending on types and types depending on types, for instance. Barendregt, in "Lambda Calculi with Types" showed that all eight combinations could be expressed within a single unified system of derivation rules and laid them out on the λ-Cube (also known as the Barendregt Cube)1:
Figure 1: The λ Cube
Here, the system \(\lambda\omega\) is a combination of \(\lambda 2\) and \(\lambda \underline{\omega}\), \(\lambda P2\) is a combination of \(\lambda 2\) & \(\lambda P\), \(\lambda P \underline{\omega}\) is a combination of \(\lambda \underline{\omega}\) & \(\lambda P\), and, at the far corner, \(\lambda C\) is a combination of all three.
Here is the unified set of derivation rules for the \(\lambda\) Cube:
\begin{align} &\textbf{(sort) } &\emptyset\vdash\ast:\square \nonumber\\ \nonumber\\ &\textbf{(var) } &\frac{\Gamma\vdash A:s}{\Gamma,x:A\vdash x:A}\text{ if }x\not\in\Gamma \nonumber\\ \nonumber\\ &\textbf{(weak) } &\frac{\Gamma\vdash A:B \enspace \Gamma\vdash C:s}{\Gamma,x:C\vdash A:B}\text{ if }x\not\in\Gamma \nonumber\\ \nonumber\\ &\textbf{(form) } &\frac{\Gamma\vdash A:s_1 \quad \Gamma, x:A\vdash B:s_2}{\Gamma\vdash \Pi.x:A.B:s_2} \nonumber\\ \nonumber\\ &\textbf{(appl) } &\frac{\Gamma\vdash M:\Pi x:A.B \quad \Gamma\vdash N:A}{\Gamma\vdash MN:B[x:=N]}\nonumber\\ \nonumber\\ &\textbf{(abst) } &\frac{\Gamma,x:A\vdash M:B \quad \Gamma\vdash \Pi x:A.B:s}{\Gamma\vdash\lambda x:A.M:\Pi x:A.B} \nonumber\\ \nonumber\\ &\textbf{(conv) } &\frac{\Gamma\vdash A:B \enspace \Gamma\vdash B^{\prime}:s}{\Gamma\vdash A:B^{\prime}}\text{ if }B\equiv_{\beta}B^{\prime} \end{align}Note in particular the (form) rule– this is the heart of the matter. We're now letting the sorts of both the variable being abstracted over and the abstraction body vary independently. By limiting the range of each term in the formation rule in different ways, we obtain the eight different type systems in the λ Cube. The following table displays the eight type systems along with the permissible values for \(s_1,s_2\) in the formation rule:
system | ||||
---|---|---|---|---|
\(\lambda\rightarrow\) | (∗,∗) | |||
\(\lambda 2\) | (∗,∗) | \((\square,\ast)\) | ||
\(\lambda \underline{\omega}\) | (∗,∗) | \((\square,\square)\) | ||
\(\lambda P\) | (∗,∗) | \((\ast,\square)\) | ||
\(\lambda\omega\) | (∗,∗) | \((\square,\ast)\) | \((\square,\square)\) | |
\(\lambda P2\) | (∗,∗) | \((\square,\ast)\) | \((\ast,\square)\) | |
\(\lambda P \underline{\omega}\) | (∗,∗) | \((\square,\square)\) | \((\ast,\square)\) | |
\(\lambda P\omega=\lambda C\) | (∗,∗) | \((\square,\ast)\) | \((\square,\square)\) | \((\ast,\square)\) |
Showing that the eight rules listed here reduce to those for each of the four type systems we've covered for appropriate choices of \(s_1\) & \(s_2\) is left as an exercise for the reader (see Remark 6.2.3 in TTFP for a sketch of the demonstration).
It should be noted that this is in no way a complete accounting of the type systems that have been built on top of the λ Calculus. In particular, the λ Cube itself has been generalized to Pure Type Systems (PTS) which allow an arbitrary number of sorts.
\(\lambda C\)
\(\lambda C\), also known as the Calculus of Constructions, sits at the far end of the λ Cube. It was developed in the eighties by Thierry Coquand (one of the originators of the Coq proof assistant).
The easiest way to conceptualize \(\lambda C\) itself is probably as an extension of \(\lambda P\) by adding types depending on types. In terms of the formation rule, \(s_1\) & \(s_2\) are free to vary over \(\{\ast,\square\}\) independently.
Terms continue to be strongly normalizing. Well-typedness & type checking continue to be decidable, though term finding is undecidable (as it is everywhere outside \(\lambda\rightarrow\) & \(\lambda \underline{\omega}\)).
Unsurprisingly, \(\lambda C\) is considerably more expressive than the systems we've seen so far. We can now encode predicate logic with not only implication & universal quantification, but negation, conjunction, disjunction and existential quantification as well.
Where Are We?
I started this series of posts looking to understand Coq in terms of first principles. Since Coq is at heart a system for writing down terms in a certain formal language, I went back to the beginning: Church's λ Calculus. I worked from there in an exercise in the Feynman technique, and we're almost to Coq's formal language. Coq extends \(\lambda C\) slightly to include inductive types in a system called the Calculus of Inductive Constructions.
It was only when we got to the last post that we were finally in a position to see exactly how "proof as term finding" actually worked, and I found myself wondering: why use type theory for this? I'm still not entirely clear, but it's undeniable that to date, the latter has been predominant in the development of proof assistants such as Coq.
Both approaches seem to date back to the early seventies; Mizar (which is based on set theory rather than type theory) dates back to 1973. Per TTFP: "The first system to actually use type theory to formalise mathematics was the Automath system, developed by N.G. de Bruijn and his research team in the early 1970s… There are various Automath type theories and several of them have been implemented as proof checkers."
The abovelinked MathOverflow question "What makes dependent type theory more suitable than set theory for proof assistants?" suggests that the real driver lies in the needs of the higher-level language that proof assistants use to define the mathematical objects about which the user is reasoning. This is generally known as the "vernacular", as opposed to the the smaller, simpler language used in the proof assistant kernel for proof checking. Because type theory can describe objects and sets with greater generality than set theory with predicate logic, the a proof assistant's vernacular almost has to be based on either type theory, or an extension to set theory with predicates that "looks a lot like type theory." At which point, "why bother with the set-theoretic kernel?"
Regardless, I think this post will wrap-up the series for a bit. A really complete treatment would show how we encode things like negation, conjunction & so forth in \(\lambda C\), perhaps along with some more worked exercises in proof as term finding, but I think I got what I needed in the last post around \(\lambda P\). My theoretical itch scratched, my plan now is to turn to the practical aspects of using Coq in anger.
05/28/24 18:30
Footnotes:
By Tellofou - Own work, CC BY-SA 4.0, https://commons.wikimedia.org/w/index.php?curid=76344034