mathlib-CPP2019
mathlib library, prepared for the paper "Formalizing computability theory via partial recursive functions"
view repo
We present a formalization of the foundations of computability theory in the Lean theorem prover. We use primitive recursive functions and partial recursive functions as the main objects of study, including the construction of a universal partial recursive function and a proof of the undecidability of the halting problem. Type class inference provides a transparent way to supply Gödel numberings where needed and encapsulate the encoding details.
READ FULL TEXT VIEW PDF
It is well known that many theorems in recursion theory can be "relativi...
read it
We translate the usual class of partial/primitive recursive functions to...
read it
It is quite well-known from Kurt Gödel's (1931) ground-breaking result o...
read it
We formalise the undecidability of solvability of Diophantine equations,...
read it
Guided by Tarksi's fixpoint theorem in order theory, we show how to deri...
read it
We establish primitive recursive versions of some known facts about
comp...
read it
The purpose of this paper is to clarify the relationship between various...
read it
mathlib library, prepared for the paper "Formalizing computability theory via partial recursive functions"
Computability theory is the study of the limitations of computers, first brought into focus in the 1930’s by Alan Turing by his discoveries on the existence of universal Turing machines and the unsolvability of the halting problem
(Turing, 1937). In the following years Alonzo Church described (Church, 1936) the -calculus as a model of computation, and Kleene proposed the -recursive functions; that these all give the same collection of “computable functions” gave credence to the thesis (Kleene, 1943) that this is the “right” notion of computation, and all others are equivalent in power. Today, this work lies at the basis of programming language semantics and the mathematical analysis of computers.Complexity theory is in some sense a refinement of computability theory, in asking not “what can be computed” but “what can be computed in a reasonable time”. Methods of asymptotic analysis of algorithms are now commonplace in computer science, and problems such as
, with its million dollar bounty, have spurred a great deal of research on classification of the difficulty of decidable problems. But this theory is still almost completely unformalized, and in this paper we aim to make some initial steps toward a flexible and usable foundation for this research. We will not cover any complexity theory in this paper, but our experiments in computability theory are very promising, and much of the infrastructure described here will be directly applicable or easily adapted.Like many areas of mathematics, both computability theory and complexity theory remain somewhat “formally ambiguous” about their foundations, in the sense that most theorems and proofs can be stated with respect to a number of different concretizations of the ideas in play. For example, in the equation , what is ? It is the set of polynomial time computable problems or languages, but whether a “language” is defined as a subset of or a subset of seems not to matter too much, and an individual author may choose the representation that is most convenient for the present purpose.
This formal ambiguity is somewhat frustrating for a formalizer, who would prefer some universal conventions, but it also provides some freedom to pick the representation that fits best with the formal system. This is seen even more prominently in computability theory, where we have three or four competing formulations of “computable”, which are all equivalent but each present their own view on the concept.
As a pragmatic matter, Turing machines have become the de facto standard formulation of computable functions, but they are also notorious for requiring a lot of tedious encoding in order to get the theory off the ground, to the extent that the term “Turing tarpit” is now used for languages in which “everything is possible but nothing of interest is easy” (Perlis, 1982). Asperti and Riccoti (Asperti and Ricciotti, 2012) have formalized the construction of a universal Turing machine in Matita, but the encoding details make the process long and arduous. Norrish (Norrish, 2011) uses the lambda calculus in HOL4, which is cleaner but still requires some complications with respect to the handling of partiality and type dependence.
Instead, we build our theory on Kleene’s theory of -recursive functions. In this theory, we have a collection of functions , in which we can do elementary operations on plus the ability to do recursive constructions on the natural number arguments. This produces the primitive recursive functions, and adding an unbounded recursion operator gives these functions the same expressive power as Turing computable functions. We hope to show that the “main result” here, the existence of a universal machine, is easiest to achieve over the partial recursive functions, and moreover the usage of typeclasses for Gödel numbering provides a rich and flexible language for discussing computability over arbitrary types.
This theory has been developed in the Lean theorem prover, a relatively young proof system based on dependent type theory with inductive types, written primarily by Leonardo de Moura at Microsoft Research (de Moura et al., 2015). The full development is available in the mathlib standard library (Carneiro et al., 2018), and a snapshot of the library as of this publication is available at (Carneiro, 2018). In section 2 we describe our extensible approach to Gödel numbering, in section 3 we look at primitive recursive functions, extended to partial recursive functions in section 4. Section 5 deals with the universal partial recursive function and its properties, including its application to unsolvability of the halting problem.
As mentioned in the introduction, we would like to support some level of formal ambiguity when encoding problems, such as defining languages as subsets of vs. subsets of , or even where is some finite or countable alphabet. Similarly, we would like to talk about primitive recursive functions of type , or the partial recursive function that evaluates a partial function specified by a code (see section 5).
Unfortunately it is not enough just to know that these types are countable, because while the exact bijection to doesn’t matter too much, it is important that we not use one bijection in a proof and a different bijection in the next proof, because these differ by an automorphism of
which may not be computable. (For example, if we encode the halting Turing machines as even numbers and the non-halting ones as odd numbers, and then the halting problem becomes trivial.) In complexity theory it becomes even more important that these bijections are “simple” and do not smuggle in any additional computational power.
To support these uses, we make use of Lean’s typeclass resolution mechanism, which is a way of inferring structure on types in a syntax-directed way. The major advantage of this approach is that it allows us to fix a uniform encoding that we can then apply to all types constructed from a few basic building blocks, which avoids the multiple encoding problem, and still lets us use the types we would like to (or even construct new types like whose explicit structure reflects the inductive construction of partial recursive functions, rather than the encoding details).
0 | 1 | 2 | 3 | … | |
---|---|---|---|---|---|
0 | 0 | 1 | 4 | 9 | |
1 | 2 | 3 | 5 | 10 | |
2 | 6 | 7 | 8 | 11 | |
3 | 12 | 13 | 14 | 15 | |
At the core of this is the function , and its inverse forming a bijection (see figure 1). There is very little we need about these functions except their definability, and that and the two components of are primitive recursive.
We say that a type is encodable if we have a function , and a partial inverse which correctly decodes any value in the image of . Here is the type consisting of the elements for , and an extra element representing failure or undefinedness. If the function happens to be total (that is, never returns ), then is called denumerable. Importantly, these notions are “data” in the sense that they impose additional structure on the type – there are nonequivalent ways for a type to be , and we will want these properties to be inferred in a consistent way.
Classically, an instance on is just an injection to , and a denumerable instance is just a bijection to . But these notions have additional constructive import, and they lie in the executable fragment of Lean, meaning that one can actually run these encoding functions on concrete values of the types, i.e. we can evaluate .
The traditional definition of primitive recursive functions looks something like this:
The primitive recursive functions are the least subset of functions satisfying the following conditions:
The function is prim. rec.
The function is prim. rec.
The function is prim. rec. for each .
If and for are prim. rec., then so is the -way composition .
If and are prim. rec., then the function defined by
is also prim. rec.
Lean is quite good at expressing these kinds of constructions as inductively defined predicates. See figure 3 for the definition that appears in Lean. But there is an important difference in this formulation: rather than dealing with -ary functions, we utilize the pairing function on to write everything as a function with only one argument. This drastically simplifies the composition rule to just the usual function composition, and in the primitive recursion rule we need only one auxiliary parameter rather than . Then the projection functions are replaced with the left and right cases for the components of , and in order to express composition with higher arity functions, we need the pair constructor to explicitly form the map . (See section 3.1 if you think this definition is a cheat.)
Now that we have a definition of primitive recursive on , we would like to extend it to other types using the mechanism discussed in section 2. There is a problem though, because given an arbitrary instance we can combine the with the function defined on induced by this instance to form a new function , which may or may not be primitive recursive. If it is not then it “brings new power” to the primitive recursive functions and so it isn’t a pure translation of primrec to other types. To resolve this we define ”primcodable α” to mean exactly that has an instance for which this composition is primitive recursive. All of the constructions we have discussed (indeed, all those defined in Lean) are , so this is not a severe restriction.
Now we can say that a function between arbitrary primcodable types is primitive recursive if when we pass through the and functions we get a primitive recursive function on :
Notation note: The dot notation expands to ”(option.map f (decode α n))”, which lifts to a function on option types before applying it to . The result has type , which has an function because does.
Now we are in a position to recover the textbook definition of primitive recursive, because is , so we have the language to say that is primitive recursive, and indeed this is equivalent to definition 3.1.
But we can now say much more: The function is primitive recursive because it is just encoded as . The constant function is primitive recursive because it encodes to some constant function (composed with a function that filters out values not in the domain ). The composition of prim. rec. functions on arbitrary types is prim. rec. The pair of primitive recursive functions , where and , is primitive recursive.
Indeed all the usual basic operations on inductive types like sum, prod, and option are primitive recursive. We define convenient syntax for prim. rec. binary functions (a common case), expressed by uncurrying to , and for primitive recursive predicates , which are decidable predicates which are primitive recursive when coerced to (which is ).
The big caveat comes in theorems like the following:
If and are types and and are prim. rec., then the function defined by
is also prim. rec.
This is of course just the generalization of the primitive recursion clause to arbitrary types, but it requires that the target type be , which means in particular that it is countable, so we can’t define an object of function type by recursion. (The universal partial recursive function will give us a way to get around this later.) But this is in some sense “working as intended”, since this is exactly why the Ackermann function
is not primitive recursive. In addition to allowing such higher types in recursion, Lean’s recursor for the natural numbers is dependent, but there is no reasonable way to incorporate dependencies in types, so we just use types when necessary.
One other type we have not yet discussed is , the type of finite lists of values of type . The and functions are defined recursively via the bijection . Even without using this instance, we can prove that any function is prim. rec. when is finite, by getting the elements of as a list, and writing as the composition of an index lookup of in and the th element function in to map to .
But once we allow the list itself to be an input, we get some more interesting possibilities. In particular, the function , which gets an element from a list by index (or returns if the index is out of bounds), is primitive recursive, and this fact expresses an equivalent of Gödel’s sequence number theorem (Gödel, 1931) (for a different encoding than Gödel’s original encoding). From this we can prove the following “strong recursion” theorem:
Ignoring the parameter , the main hypothesis says essentially that , where the first values of have been written in a list (and the length of the list tells what value of we are constructing). The reason has optional return value is to allow for it to fail when the input is not valid.
Once we have lists, the dependent type is just a subtype of , so it has an easy
instance, and most of the vector functions follow from their list counterparts. Similarly for functions
, which are isomorphic to .Now that we have a proper theory, we can return to the question of how to show equivalence to definition 3.1. We do this by defining ”nat.primrec’ : ∀ n, (vector ℕ n → ℕ) → Prop” with only 5 clauses matching definition 3.1. It is easy to show at this point that implies , since all of the functions appearing in definition 3.1 are known to be primitive recursive. For the converse, most of the clauses are easy, but our earlier cheat was to axiomatize that mkpair and unpair are primitive recursive, even though the definition involves addition, multiplication and case analysis in mkpair and even square root in the inverse function:
(Here ”sqrt : ℕ → ℕ” is actually the function .) So we must show that all these operations are primitive recursive by the textbook definition. The square root case is not as difficult as it may sound; since it grows by at most 1 at each step we can define it by primitive recursion as
This alternate basis for primrec is useful for reductions, for example, to show that some other basis for computation like Turing machines can simulate every primitive recursive function.
The partial recursive functions are an extension of primitive recursive functions by adding an operator , where is a predicate, which denotes the least value of such that is true. Intuitively, this value is found by starting at 0 and testing ever larger values until a satisfying instance is found. This function is not always defined, in the sense that even when all the inputs are well typed it may not return a value – it can result in an “infinite loop”.
So before we tackle the partial recursive functions we must understand partiality itself, and in particular how to represent unbounded computation, computably, in a proof assistant that can only represent terminating computations (Lean is based on dependent type theory, which is strongly normalizing, so all expression evaluation terminates).
We have already discussed the type for representing a possible failure state, but nontermination is a slightly different kind of “failure” in that you can’t tell that you have failed while executing the program, and this difference makes itself known in the type system.
To address this distinction, we introduce the type:
A value of type is a nondecidable optional value, in the sense that there is not necessarily a decision procedure for determining if the contains a value, but if it does then you can extract the value using the function component. This type has a monad structure, as follows:
Also, there is an element representing an undefined value. We can map by sending to and to , and assuming the law of excluded middle we can also define an inverse map and show , but this breaks the computational interpretation of .
The definition of bind, also written in Haskell style as the infix operator >>=, is a bit complicated to write but is “exactly what you would expect” in terms of its behavior. Given a partial value and a function , the resulting partial value is defined when is defined to be some , and is defined, in which case it evaluates to .
It is convenient to abstract from the definition to a relational version, where means – that is, says that is defined and equal to . With this definition the bind operator can be much more easily expressed by the theorem
which is shared with many other collection-based monad structures. Because they come up often, we will use the notation for the type of all partial functions from to .
One important function that is (constructively) definable on this type is fix, which has the following properties:
Given an input , it evaluates to get either or . In the first case it returns , and in the second case it starts over with the value . The function is defined when this process eventually terminates with a value, if we assume this then we can construct the value that returns. So even though Lean’s type theory does not permit unbounded recursion, by working in this partiality monad we get computable unbounded recursion.
The minimization operator , which finds the smallest value satisfying the (partial) boolean predicate can be defined in terms of fix as follows:
The definition nat.partrec is given in figure 4. The first 7 cases are almost the same as those of primrec, except that we must now worry about partiality in all the operations that build functions. So for example λ n, mkpair <$> f n <*> g n is the function except that if the computation of either or fails to return a value, then this is not defined. (In other words, this operation is “strict” in both arguments). Similarly, the composition is now expressed as λ n, g n >>= f, which says that should be evaluated first, and if it is defined and equals , then is the resulting value.
The interesting case is the last one, which incorporates the rfind function on . Ignoring partiality, it says that is partial recursive if is. This is of course the source of the partiality – all the other constructors produce total functions from total functions but this can be partial if the function is never zero.
Although this defines a class of partial functions, some of the functions happen to be total anyway, and we call a total partial-recursive function computable. It is an easy fact that every primitive recursive function is computable.
As before, we can compose with and to extend these definitions to any type. Although we could define an analogue of using computable functions instead of primitive recursive functions, since we want to stick to simple encodings (usually not just primitive recursive but polynomial time), and we already have encodings for all the important types, so is enough.
One aspect of this definition which is not obviously a problem until one works out all the details is the strictness of the prec constructor. In conventional notation, it says that if and are partial recursive functions, then so is the function defined by
Importantly, is only defined if is defined and is defined. It does not matter if does not make use of the argument at all, for example if it is the first projection. This comes up in the definition of the lazy conditional , defined when , by:
where in particular regardless of whether is defined. This is the basis of “if statements” that resemble execution paths in a computer – we need a way to choose which subcomputation to perform, without needing to evaluate both. The usual way of implementing is to use primitive recursion on the argument , using in the zero case and in the successor case. But because of the strictness constraint, this will result in (where represents an undefined value or infinite loop). In fact, we won’t have the tools to solve this problem until section 5.3.
Because partrec is an inductive predicate, there is a natural data type that corresponds to proofs that a function is partial recursive:
We can define the semantics of a code via an “evaluation” function that takes a code and an input value in and produces a partial value.
Then it is a simple consequence of the definition that is partial recursive iff there exists a code such that .
Note: The constructor is a slightly modified version of which is easier to use in evaluation:
which can be expressed in terms of as:
So we can pretend that partrec was defined with a case for instead of since it yields the same class of functions.
Now the key fact is that is denumerable. Concretely, we can encode it using a combination of the tricks we used to encode sums, products and option types, that is,
where is the pairing function from figure 1. (We could have used a more permissive encoding, but this has the advantage that it is a bijection to , which makes the proof that this is a type trivial.)
Having shown that the type is we can now start to show that functions on codes are primitive recursive. In particular, all the constructors are primitive recursive, the recursion principle preserves primitive recursiveness and computability (not partial recursiveness, because of the as-yet unresolved problem with ), and we can prove that these simple functions on codes are primitive recursive:
In particular, the rather understated fact that is primitive recursive is a form of the -- theorem of recursion theory.
We have one more component before the universality theorem. We define a “resource-bounded” version of , namely where . (In the formal text it is called evaln.) This function is total – we have a definite failure condition this time, unlike itself, which can diverge. There are multiple ways to define this function; the important part is that if then for all , and if is defined then for some . Furthermore, it is convenient to ensure that is monotonic in , and the domain of is contained in , that is, if then .
The Lean definition of evaln is given in figure 5. The details of the definition are not so important, but it is interesting to note that our “fuel” for the computation need only decrease when we evaluate a function which does not decrease the size of the program that is being computed, namely in the prec and rfind’ cases. (You may wonder why we cannot use the fact that is decreasing in the prec case to prove termination, but this is because the function is not defined by recursion on , it is by recursion on at all simultaneously.)
Because has finite domain outside which it is , we can encode the whole function as a single . Thus we can pack the function into the type , and define this by strong recursion (using the theorem nat_strong_rec mentioned in section 3), since in every case of the recursion, either decreases and remains fixed, or decreases and remains fixed.
Thus is primitive recursive (jointly in all arguments), and since where , this shows that is partial recursive. This is (more or less) Kleene’s normal form theorem – is a universal partial recursive function.
An easy consequence of universality are the fixed point theorems:
If is computable, then there exists some code such that .
Consider the function defined by (using to use natural numbers as codes in ). This function is clearly partial recursive, so let . Now let such that ; then is computable so let . Then for we have:
∎
If is partial recursive, then there exists some code such that .
We can also finally solve the problem. If and are partial recursive functions, then letting and , the function
is primitive recursive (since both branches are just numbers now instead of computations that may not halt), and . More generally, this implies that we can evaluate conditionals where the condition is a computable function and the branches are partial functions. We conclude with Rice’s theorem on the noncomputability of all nontrivial properties about computable functions:
Let such that is computable. Then for any , implies (so classically ).
Apply theorem 5.2 to the function to obtain a such that . (Note is decidable because it is computable.) Then if , we have for all so , hence . And if then similarly which contradicts , . ∎
The undecidability of the halting problem is a trivial corollary:
The set
is not computable.
Suppose it is; we can write it as where , so applying Rice’s theorem with and we have a contradiction from and . ∎
The most obvious next step is to show the equivalence of other formulations of computable functions: Turing machines, -calculus, Minsky register machines, C… the space of options is very wide here and it is easy to get carried away. Furthermore, if one holds to the thesis that partial recursive functions are the quickest lifeline out of the Turing tarpit, then one must acknowledge that this is to jump right back in, where the hardest part of the translation is fiddling with the intricacies of the target language. We are still looking for ways to do this in a more abstract way that avoids the pain.
As mentioned in the introduction, this project was explicitly for the purpose of setting up the foundations of complexity theory. One of the often stated reasons for choosing Turing machines over other models of computation like primitive recursion is because they have a better time model. We would argue that this is not true at fine grained notions of complexity (because there is often a linear multiplicative overhead for running across the tape compared to memory models). Moreover, in the other direction we find that, at least in the case of polynomial time complexity, there are methods such as bounded recursion on notation (Hofmann, 2000) that generalize primitive recursion methods to the definition of polynomial time computable functions, which can be used to define , and -hardness at least; we are hopeful that these methods can extend to other classes, possibly by hybridizing with other models of computation as well.
Comments
There are no comments yet.