Recursion vs. Iteration: - The Original Lisp Language Was Truly A Functional Language
Recursion vs. Iteration: - The Original Lisp Language Was Truly A Functional Language
Iteration
• The original Lisp language was truly a functional
language:
– Everything was expressed as functions
– No local variables
– No iteration
• You had to use recursion to get around these problems
– Although they weren’t considered problems, functions are
usually described recursively, and since the vision of Lisp was
to model mathematical functions in a language, this was
appropriate
• But recursion is hard, in CL why should we use it?
• Can’t we just use iteration?
Should we avoid recursive?
• Recursion is hard:
– It is hard to conceptualize how a problem can be solved
recursively
– Once implemented, it is often very difficult to debug a
recursive program
– When reading recursive code, it is sometimes hard to really see
how it solves the problem
• Recursion is inefficient:
– Every time we recurse, we are doing another function call, this
results in manipulating the run-time stack in memory, passing
parameters, and transferring control
• So recursion costs us both in time and memory usage
– Consider the example on the next slide which compares
iterative and recursive factorial solutions
A Simple Comparison
(defun ifact (n) Here, the function is
(let ((product 1)) called once, there are two
(do ((j 0 (+ 1 j))) ((= j n)) local variables
(setf product (* product (+ j 1))))
product)) The loop does a comparison
and if the terminating
condition is not yet true, we
branch back up to the top
(defun rfact (n)
(if (< n 1) 1 (* n (rfact (- n 1))))) Total instructions: n * 5 + 3
Here, we have less code, no local variables (only a parameter) and fewer total
instructions in the code, each iteration has a comparison and then either returns
1 or performs 3 function calls (-, rfact, * in that order)
But we aren’t seeing the stack manipulations which require pushing a new n,
space for the function’s return value, and updating the stack pointer register,
and popping off the return value and n when done
Why Recursion?
• If recursion is harder to understand and less efficient,
why use it?
– It leads to elegant solutions – less code, less need for local
variables, etc
– If we can define a function mathematically, the solution is easy
to codify
– Some problems require recursion
• Tree traversals
• Graph traversals
• Search problems
• Some sorting algorithms (quicksort, mergesort)
– Note: this is not strictly speaking true, we can accomplish a solution
without recursion by using iteration and a stack, but in effect we would be
simulating recursion, so why not use it?
• In some cases, an algorithm with a recursive solution leads to a lesser
computational complexity than an algorithm without recursion
– Compare Insertion Sort to Merge Sort for example
Lisp is Set Up For Recursion
• As stated earlier, the original intention of Lisp
was to model mathematical functions so the
language calls for using recursion
– Basic form:
(defun name (params)
(if (terminating condition)
return-base-case-value
(name (manipulate params))))
– The components here are to test for a base case and
if true, return the base cases’ value, otherwise
recurse passing the function the parameter(s)
manipulated for the next level
What Happens During Recursion
• You should have studied this in 262,
Run-time stack:
but as a refresher:
– We use the run-time stack to coordinate main calls m1
recursion m1 calls m2
– The stack gives us a LIFO access m2 calls m3
m3 calls m4
• Imagine that function1 calls function2 which
calls function3 We are currently in m4
• When function3 ends, where do we return to?
– the run-time stack stores the location in Main
function2 to return to
• When function2 ends, where do we return to? m1
– the run-time stack stores the location in
function1 to return to m2
– etc
– Using a stack makes it easy to m3
“backtrack” to the proper location when a
method ends m4
• Notice that we want this behavior whether we stack
are doing normal function calls or recursion pointer
More On the Run-time Stack
• For each active function, the run-time stack stores an “activation
record instance”
– This is a description of the function’s execution and stores
• Local variables, Parameters, Return value
• Return pointer (where to return to in the calling function upon function termination)
• Every time a function (or method in Java) is called
– the run-time stack is manipulated by pushing a new activation record instance
onto it
– proper memory space is allocated on the stack for all local variables and
parameters
– the return pointer is set up
– the stack pointer register is adjusted
• Every time a function terminates
– the run-time stack has the top activation record instance popped off of it,
returning the value that the function returns
– the PC (program counter register) is adjusted to the proper location in the calling
function
– the stack pointer register is adjusted
An Example
AR for factorial
AR for factorial
n=1
n=2
return value: 1
return value: 2
return to: (fact 2) *
return to: (fact 3) *
AR for factorial
n=2 AR for factorial AR for factorial
return value: n=3 n=3
return to: (fact 3) * return value: ___ return value: 6
return to: interpreter return to: interpreter
AR for factorial
n=3
return value: ___ 6 is returned and
return to: interpreter printed in the
interpreter
Lisp Makes Recursion Easy
• Well, strictly speaking, recursion in Lisp is similar to recursion in
any language
• What Lisp can do for us is give us easy access to the debugger
– You can insert a (break) instruction which forces the evaluation step of the
REPL cycle to stop executing, leaving us in the debugger
– Or, if you have a run-time error, you are automatically placed into the
debugger
– From the debugger you can
• inspect the run-time stack to see what values are there
• return to a previous level of recursive call
• provide a value to be returned
– Thus, you can either determine
• why you got an error by inspecting the stack
• see what is going on in the program by inspecting the stack
• return from an error by inserting a partial or complete solution
• CL can also make a recursive program more efficient (to be
explained later)
Examples of Recursive Code
• Every List function (defun last (lis)
in CL can be (cond ((null (cdr lis)) (car lis))
implemented (t (last (cdr lis)))))
recursively (defun last2 (lis)
– whether they are or (cond
not I’m not sure, but ((and (listp lis) (= (length lis) 1)) lis)
probably they are (t (last2 (cdr lis)))))
• Here we start with 3 (defun last3 (lis)
versions of last (cond ((atom lis) (list lis))
((and (listp lis) (= (length lis) 1)) lis)
(t (last3 (cdr lis)))))
Notice the nil inserted into the list because we replace (a) with nil, can
we fix this? If so, how?
Towers of Hanoi
Towers of Hanoi with 4 disks
Partial solution
Start Intermediate Final
(defun hanoi (n a b c)
(cond ((= n 1)
(print (list ’move n ’from a ’to c))
’done) ;; used so that the last message is not
;; repeated as the return value of the function
(t (hanoi (- n 1) a c b)
(print (list ’move n ’from a ’to c))
(hanoi (- n 1) b a c))))
Tail Recursion
• When writing recursive code, we typically write the
recursive function call in a cond statement as:
– (t (name (manipulate params)))
• If the last thing that this function does is call itself, then
this is known as tail recursion
– Tail recursion is important because it can be implemented
more efficiently
– Consider the following implementation of factorial, why isn’t
it tail recursive?