# Lisp's grandfather paradox

Recursion is an idea often associated with infinity, a process that repeats ad aeternam. It is perhaps surprising, then, that the origin story of mathematical recursion is grounded in finitism, the rejection of all things infinite.

In 1923, Skolem developed a theory of recursive functions as a means of avoiding the so-called paradoxes of the infinite in his paper Begründung der elementären Arithmetik durch die rekurrierende Denkweise ohne Anwendung scheinbarer Veränderlichen mit unendlichem Ausdehnugsbereich

^{1}(Skolem, T., 1923)

Primitive recursion was originally introduced by Dedekind in 1888
(Dedekind, Richard, 1965), and proposed as a basis for arithmetic by Skolem
in 1923. It is perhaps Gödel's contributions to recursion theory that
had the biggest impact, because it gave a precise meaning to the
notion of computability. (Gödel, Kurt, 1931) Finally, Rózsa Péter is the
mathematician responsible for coining the term *primitive recursion*
and for writing the first monograph dedicated to recursive
functions. (Lambek, J., 1968)

We believe that programming languages are the best way to convey the concept of recursion. They share with mathematics the ability to give a formal meaning to a set of symbols. But unlike mathematics, programming languages can be directly experienced-you can take the programs in this book, observe their behavior, modify them and experience the effect of these modifications. (Friedman, Daniel P. and Felleisen, Matthias, 1996)

In *The Little Schemer*, Daniel Friedman and Matthias Felleisen taught
recursion in Scheme through dialogue, arguing that the requirements
to be a successful learner was not analytical reasoning but pattern
recognition. To understand the practicalities of recursion in Lisp,
*The Little Schemer* is indeed a great reference.

Contrary to recursion in Scheme, which is defined on symbolic
expressions, primitive recursion is defined on the natural
numbers. There is no lack of mathematical resources describing
primitive recursion, but we are going to try and build an
*intuition* for it by faithfully translating the rules of
primitive recursion into a computer program.

The goal of the exercise that follows is an understanding of the
framework of primitive recursion in the terms within which it
appeared. The motivation for this endeavor lies in a quest to tackle
the *big ideas* at the crossroads of Logic, Mathematics and Computer
Science, such as:

*Grundlagenkrise der Mathematik*- Finitism
- Gödel's arithmetization of syntax
- Recursion theory
- The origins of Lisp

It might be interesting to start with a quick glance at the traditional presentation of the topic. Wikipedia has a decent entry, Stanford Encyclopedia of Philosophy too. Take a look at the formulas describing primitive recursion, no matter how opaque. At the end of our exercise, we will have a basis of comparison.

*Note:* We are going to stick close to some conventions found in the
traditional treatment of the topic, up to the capitalized one-letter
naming scheme for functions. This will make it easy for learners to
switch back and forth between the code and the formalism. Please do
not hesitate to replace the one-letter function names with something
more palatable. And remember that you can trace the execution of the
functions in the REPL.

Primitive recursion takes only three initial functions closed under two operations to produce all of the familiar functions on the natural numbers.

… most computable functions that are studied in number theory (and more generally in mathematics) are primitive recursive. For example, addition and division, the factorial and exponential function, and the function which returns the nth prime are all primitive recursive. (Brainerd, Walter S. and Landweber, Lawrence H., 1974)

Our first rule is the Zero rule.

(def Z #(fn [& _] 0))

This function returns a function that takes any number of arguments and always returns zero.

[((Z)) ((Z) 5) ((Z) 1 2 3)]

[0 0 0]

Our second basic function is the successor function. Yes, that one from number theory. It increments a number by one.

Being identical to `inc`

in Clojure, we will just alias it.

(def S inc)

Our last basic function is trickier to explain because its purpose may not be immediately apparent. It is called the projection function, and it picks an element from a sequence. Its role is auxiliary, but nonetheless crucial. It is used to configure primitive recursion by rearranging function arguments. We will see it in action soon.

(defn P [i] (fn [& args] (nth args (dec i))))

*Note:* The projection function is not zero-based, but the ArraySeq
containing the arguments is, so we make the decrement adjustment.

Our first operation is composition. We will call it capital C. Remember, the initial or basic functions operate on natural numbers, the operations act on those initial functions.

(defn C [f & gs] (fn [& xs] (apply f (map #(apply % xs) gs))))

Composition takes a function 𝑓 and any number of functions 𝑔, and returns a function that receives any number of arguments and applies f to the result of applying functions 𝑔 on those arguments.

In some textbooks it is called the substitution operator; it is used for substituting constants, permuting variable and composing functions.

The following usage example may help.

((C S (P 1)) 5 11)

6

Contrast with:

((C S (P 2)) 5 11)

12

Finally, the recursion operation.

(defn R [n & xs] (fn [f g] (loop [i 1 j 0 acc (apply f xs)] (if (<= i n) (recur (inc i) (inc j) (apply (partial g j acc) xs)) acc))))

Function 𝑓 is used for the initial computation only. After such initialization, function 𝑔 is proceeding by applying its rules, whatever they may be, on the previous computed value. Hence, the recursion.

The most important thing to notice here is that the recursion is bounded. In other words, we know in advance the number of times the procedure will repeat itself, namely, 𝐧 number of times. Primitive recursion is always bounded, it is guaranteed to halt.

Now that we have the pieces in place we can start to build our algebra of primitive recursive functions. Addition is achieved by applying the successor function 𝐱 times on 𝐲 + 1.

(defn add [x y] (let [f (P 1) g (C S (P 2))] ((R x y) f g)))

(add 4 5)

9

We can leverage addition to define multiplication.

(defn mul [x y] (let [f (Z) g (C add (P 2) (P 3))] ((R x y) f g)))

Usage:

(mul 6 7)

42

By definition, any function composed from the basic function and the operations is primitive recursive. Likewise, any derivation, like addition or multiplication, is also primitive recursive. This is how we develop the algebra of primitive recursive functions.

Note that we can generate any number with zero and the successor function. We call this the constant function 𝐤.

(defn k [n] (fn [& _] (let [f (Z) g (C S (P 1))] ((R n) f g))))

Usage:

[((k 5) 5) ((k 5) 6) ((k 5) 7)]

[5 5 5]

No matter what the input is, the function returns the number of times we applied the successor function starting from zero.

Factorial leverages multiplication, predictably. As does exponentiation.

(defn fac [n] (let [f (k 1) g (C mul (P 2) (C S (P 1)))] ((R n) f g)))

[(fac 3) (fac 4) (fac 5)]

[6 24 120]

(defn pow [x y] (let [f (k 1) g (C mul (P 2) (P 3))] ((R y x) f g)))

[(pow 3 4) (pow 4 3) (pow 4 5) (pow 5 4)]

[81 64 1024 625]

Gödel showed that a logical calculus is embedded in the primitive recursive functions. If we adopt a mathematical understanding of how logic works, we will say that zero represents false and the positive numbers represent true. Conjunction, the AND operator, is just multiplication. If you multiply two numbers the only way to get a positive number is if both of them are positive. Likewise, disjunction, the OR operator, is addition. Again, this is a basic feature of addition. The sum of two natural numbers is positive if one or the other is positive, or both.

Sometimes it is not convenient to have all the positive numbers signifying true, and we might want to normalize them to 1. We can define bool, a recursive definition of a normalizing function such that true means 1 and false means 0.

(defn bool [x] (let [f (Z) g (k 1)] ((R x) f g)))

Notice that if we invert the initialization function with the
recursive function, we get the predicate function `zero?`

which
doubles as negation, or `not`

.

(defn z [n] (let [f (k 1) g (Z)] ((R n) f g)))

Usage:

[(z 0) (z 1) (z 2)]

[1 0 0]

Putting all of the above together, we can define `and`

:

(def and (C bool mul))

And `or`

:

(def or (C bool add))

Please head over to the gists to check how we can further define if-then-else, more junctors, modulo, a predicate function that checks if a number is prime, etc.

Both a Clojure version and a Scheme version are available.

**Open for work**

Are you hiring? Do you know someone that is? Looking for a collaborator? A team lead or member? A consultant, maybe? Need a little boost or a long-term commitment? Let's talk.

Because it's possible to compose all the familiar arithmetic functions within the framework of primitive recursion, David Hilbert, who instigated the efforts to rehabilitate mathematics through finitistic means, conjectured that all computable functions were primitive recursive. In 1927, two of his students, Gabriel Sudan and Wilhelm Ackermann, proved him wrong.

(defn sudan [n x y] (cond (zero? n) (+ x y) (zero? y) x :else (recur (dec n) (sudan n x (dec y)) (+ y (sudan n x (dec y)))))) (defn ackermann [m n] (cond (zero? m) (inc n) (zero? n) (ackermann (dec m) 1) :else (ackermann (dec m) (ackermann m (dec n)))))

These functions increase super-exponentially, even for very small
input values. Running the Ackermann function with `m = 4`

results in a
*StackOverflowError* and no optimizations are straightforward to
leverage, not tail recursion, not managing the call stack manually in
heap memory.

While primitive recursion doesn't capture all the computable functions, all functions that are primitive recursive are computable. In some settings, this is a desirable property. Dennis Ritchie, author of the C programming language, designed the LOOP language, restricted to primitive recursion, in order to ask the question that otherwise would be a non-starter: can one look at a program and determine an upper bound on its running time? Then he went on formulating theorems concerning the hierarchy of primitive recursion by looking at the structure of the loop, its nesting depth and the growth rate. (Meyer, Albert R. and Ritchie, Dennis M., 1967)

Intuitively, primitive recursion corresponds to functions that can be computed with an understanding in advance about how far one must search for an answer. General computability includes functions in which one needs to search finitely far but without knowing how far that might be.

In *Gödel, Escher, Bach: an Eternal Golden Braid*, Douglas Hofstadter
devised the BlooP and the FlooP languages to illustrate the difference
between bounded loops (FOR) and unbounded loops (WHILE, GOTO),
corresponding to primitive recursion and general recursion
respectively. Adding unbounded loops makes the language general
recursive and Turing-complete, as are all real-world computer
programming languages. (Hofstadter, Douglas R., 1999)

Systems that are Turing complete, like the lambda calculus, the Turing machine or the 𝛍-recursive functions (aka general recursive functions, aka partial recursive functions), capture the notion of computable functions, but in these systems, while mathematically definable, some functions are not computable. (Rado, T., 1962)

My desire for an algebraic list processing language for artificial intelligence work on the IBM 704 computer arose in the summer of 1956 during the Dartmouth Summer Research Project on Artificial Intelligence which was the first organized study of AI… While expressions could be handled easily in FLPL, and it was used successfully for the Geometry program, it had neither conditional expressions nor recursion, and erasing list structure was handled explicitly by the program. (McCarthy, John, 1978)

John McCarthy based Lisp on general recursion, but devised it to operate on symbols rather than numbers. He was interested in AI, where concepts, the way humans manipulate them, occupy the center stage. A symbol can stand in for any idea, whether code or data. Just today we've seen just how expressive symbolic expressions are, effortlessly encoding the formulas of primitive recursion. Somehow we have made Lisp revisit its conceptual ancestor. We've traveled in time like in the grandfather paradox. Not with the intent to kill the progenitor, but rather to embrace him, bring him back to life, even if for a moment.

Wow, you've made it! Right up to the end! So you're into this kind of stuff, aren't you?! Maybe we should be friends! What's more, I might be writing more on the topic. Would you like to be notified? Also, I'm gauging interest in long-form writing, maybe a book. If that's something you'd sign up for, please let me know.

Bibliography

Brainerd, Walter S. and Landweber, Lawrence H. (1974). *Theory of Computation*, John Wiley & Sons, Inc.

Dedekind, Richard (1965). *Was sind und was sollen die Zahlen?. Stetigkeit und Irrationale Zahlen*, Vieweg+Teubner Verlag.

Friedman, Daniel P. and Felleisen, Matthias (1996). *The little Schemer (4th ed.)*, MIT Press.

Gödel, Kurt (1931). *Über formal unentscheidbare Sätze der Principia Mathematica und verwandter Systeme I*, Springer Science and Business Media LLC.

Hofstadter, Douglas R. (1999). *Gödel, Escher, Bach: An Eternal Golden Braid*, Basic Books, Inc.

Lambek, J. (1968). *Recursive functions, by Rózsa Péter. Academic Press Inc., New York, 1967. 300 pages. $13.50.*, Canadian Mathematical Bulletin.

McCarthy, John (1978). *History of LISP*, Association for Computing Machinery (ACM).

Meyer, Albert R. and Ritchie, Dennis M. (1967). *The complexity of loop programs*, ACM Press.

Rado, T. (1962). *On Non-Computable Functions*, Institute of Electrical and Electronics Engineers (IEEE).

Skolem, T. (1923). *Begründung der Elementaren Arithmetik durch die rekurrierende Denkweise ohne Anwendung scheinbarer Veränderlichen mit unendlichem Ausdehnungsbereich*.

## Footnotes:

^{1}

Justification of elementary arithmetic by the recurrent mindset without the use of apparent variables with infinite extension area.