Recursion and Iteration
In Chapter 1, Immutability, I stated that functional programming makes use of recursion in order to eliminate assignment. In this chapter, we will look at the two different varieties of recursion; one we will call iteration and the other will retain the original name: recursion.
Iteration
TCO is the remedy for the infinite stack depth implied by infinite recursive loops. However, TCO is only applicable if the recursive call is the very last thing to be executed within the function. Such functions are often called tail call functions.
Here is a very traditional implementation of a function to create a list of Fibonacci numbers:
(defn fibs-work [n i fs] (if (= i n) fs (fibs-work n (inc i) (conj fs (apply + (take-last 2 fs)))))) (defn fibs [n] (cond (< n 1) [] (= n 1) [1] :else (fibs-work n 2 [1 1])))
This program is written in Clojure, which is a variant of Lisp. You call this function like this:
(fibs 15)
And it returns an array of the first 15 Fibonacci numbers:
[1 1 2 3 5 8 13 21 34 55 89 144 233 377 610]
Many programmers experience eyestrain headaches the first few times they look at Lisp, mostly because the parentheses don’t seem to make any sense. So let me give you a very brief tutorial about those parentheses.
Very Brief Clojure Tutorial
This is a typical function call in C, C++, C#, and Java: f(x);.
Here is the same function in Lisp: (f x).
Now you know Lisp. Here ends the tutorial.
That’s not much of an exaggeration. The syntax of Lisp is really that simple.
The syntax of Clojure is just a bit more complicated. So let’s take the above program apart, one statement at a time.
First there’s defn, which looks like it is being called as a function. Let’s go with that for now. The truth is mostly compatible with that view. So the defn “function” defines a new function from its arguments. The functions being defined are named fibs-work and fibs. The square brackets after the function name enclose the names of the arguments of the function.1 So the fibs function takes a single argument named n, while the fibs-work function takes three arguments named n, i, and fs.
Following the argument list is the body of the function. So the body of the fibs function is a call to the cond function. Think of cond like a switch statement that returns a value. The fibs function returns the value returned by cond.
The arguments to cond are a set of pairs. The first element in each pair is a predicate, and the second is the value that cond will return if that predicate is true. The cond function walks down the list of pairs until it sees a true predicate, and then it returns the corresponding value.
The predicates are just function calls. The (< n 1) predicate simply calls the < function with n and 1. It returns true if n is less than 1. The (= n 1) predicate calls the = function, which returns true if its arguments are equal. The :else predicate is considered true.
The value returned by cond for the (< n 1) predicate is [], an empty vector. If (= n 1), then cond returns a vector containing 1. Otherwise, cond returns the value produced by the fibs-work function.
So, the fibs function returns [] if n is less than 1, [1] if n is equal to 1, and (fibs-work n 2 [1 1]) in every other case.
Got it? Make sure you do. Go back over it until you do.
The ))) at the end of the fibs function are just the closing parentheses of the defn, cond, and fibs-work function calls. I could have written fibs like this:
(defn fibs [n] (cond (< n 1) [] (= n 1) [1] :else (fibs-work n 2 [1 1]) ) )
Perhaps that makes you feel better. Perhaps that relieves the eyestrain headache you felt coming on. And indeed, many new Lisp programmers use this technique to reduce their parentheses anxiety. That’s certainly what I did a decade and a half ago when I first started learning Clojure.
After a few years, however, it becomes obvious that there is no reason to put trailing parentheses on their own lines, and the technique simply becomes an annoyance. Trust me. You’ll see.
Anyway, that brings us to the heart of the matter, the fibs-work function. If you have gotten comfortable with the fibs function, you have probably already worked out most of the details of the fibs-work function. But let’s go through it step by step just to be sure.
First, the arguments: [n i fs]. The n argument tells us how many Fibonacci numbers to return. The i argument is the index of the next Fibonacci number to compute. The fs argument is the current list of Fibonacci numbers.
The if function is a lot like the cond function. Think of (if p a b) as (cond p a :else b). The if function takes three arguments. It evaluates the first as a predicate. If the predicate is true, it returns the second argument; otherwise, it returns the third.
So, if (= i n), then we return fs. Otherwise… Well, let’s walk through that one carefully.
(fibs-work n (inc i) (conj fs (apply + (take-last 2 fs))))
This is a recursive call to fibs-work, passing in n unchanged, i incremented by one, and fs with a new Fibonacci number appended.
It is the conj function that does the appending. It takes two arguments: a vector and the value to append to that vector. Vectors are a kind of list. We’ll talk about them later.
The take-last function takes two arguments: a number n and a list. It returns a list containing the last n elements of the list argument.
The apply function takes two arguments: a function and a list. It calls the function with the list as its arguments. So, (apply + [3 4]) is equivalent to (+ 3 4).
OK, so now you should have a good working grasp of Clojure. There’s more to the language that we’ll encounter as we go along. But for now, let’s get back to the topic of iteration and recursion.
Iteration
Notice the recursive call to fibs-work is a tail call. The very last thing done by the fibs-work function is to call itself. Therefore, the language can employ TCO to eliminate previous stack frames and turn the recursive call into a goto, effectively converting the recursion to pure iteration.
So, then, functions that employ tail calls are, for all intents and purposes, iterative.
TCO, Clojure, and the JVM
The Java virtual machine (JVM) does not make it easy for languages to employ TCO. Indeed, the code I just showed you does not use TCO and therefore grows the stack throughout the iteration. Thus, in Clojure, we explicitly invoke TCO by using the recur function as follows:
(defn fibs-work [n i fs] (if (= i n) fs (recur n (inc i) (conj fs (apply + (take-last 2 fs))))))
The recur function can only be called from a tail position, and it effectively reinvokes the enclosing function without growing the stack.