Professional Documents
Culture Documents
Iteration
The original Lisp language was truly a functional language:
Everything was expressed as functions No local variables No iteration
But recursion is hard, in CL why should we use it? Cant we just use iteration?
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 arent seeing the stack manipulations which require pushing a new n, space for the functions return value, and updating the stack pointer register, and popping off the return value and n when done
Why Recursion?
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
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
m1
m2 m3 m4
Using a stack makes it easy to backtrack to the proper location when a method ends
Notice that we want this behavior whether we are doing normal function calls or recursion stack pointer
An Example
(defun fact (n) (if (<= n 1) 1 (* n (fact (- n 1))))) The activation record instance (AR) for fact stores three things, n, the return value, and the pointer of where to return to in the next AR when fact terminates We start with (fact 3)
AR for factorial n=3 return value: ___ return to: interpreter AR for factorial n=2 return value: ___ return to: (fact 3) * AR for factorial n=3 return value: ___ return to: interpreter
AR for factorial n=1 return value: ___ return to: (fact 2) * AR for factorial n=2 return value: ___ return to: (fact 3) * AR for factorial n=3 return value: ___ return to: interpreter
Example Continued
AR for factorial n=1 return value: 1 return to: (fact 2) * AR for factorial n=2 return value: 2 return to: (fact 3) * AR for factorial n=3 return value: ___ return to: interpreter AR for factorial n=3 return value: 6 return to: interpreter
CL can also make a recursive program more efficient (to be explained later)
The top definition returns an atom, the bottom definition can handle atoms and Lists, CLs last is probably last2
(defun mybutlast (lis) (cond ((null (cdr lis)) nil) (t (cons (car lis) (mybutlast (cdr lis))))))
(defun reverse1 (lis) (let (temp (size (length lis))) (dotimes (a size) (setf temp (append temp The recursive (list (nth (- (- size a) 1) lis))))) version, while temp) being harder to understand, (defun reverse2 (lis) contains far (if (null lis) nil less code (append (reverse2 (cdr lis)) (list (car lis)))))
The iterative version of reverse builds a list iteratively using a local variable
Member
(defun member1 (a lis) (dotimes (i (length lis)) (if (equal a (car lis)) (return lis) (setf lis (cdr lis)))))
In actuality, member does not work as indicated here because member only tests top level items using eql instead of equal
So
(member2 '(1 2) '(1 (1 2) 2)) returns ((1 2) 2)
(defun member2 (a lis) (cond ((null lis) nil) ((equal a (car lis)) lis) (t (member2 a (cdr lis)))))
While
(member '(1 2) '(1 (1 2) 2)) returns nil
(defun subs2 (a b lis) (cond ((null lis) nil) ((equal a (car lis)) (cons b (subs2 a b (cdr lis)))) (t (cons (car lis) (subs2 a b (cdr lis))))))
(defun sub-first (a b lis) (cond ((null lis) nil) ((equal a (car lis)) (cons b (cdr lis))) (t (cons (car lis) (sub-first a b (cdr lis))))))
Flattening a List
Now consider the problem of delistifying a list
That is, taking all of the items in sublists and moving them into the top-level list The recursive version is fairly straightforward
If the parameter is nil, return the empty list If the parameter is an atom, return the atom as a list Otherwise, append what we get back by recursively calling this function with the car of the parameter (null, an atom, or a list) and the cdr of the parameter (null or a list)
Since sublists may contain subsublists, etc, an iterative version would be extremely complicated!
(defun flatten (lis) (cond ((null lis) nil) (flatten (a (b c (d e) f (g)) ((h) i))) ((atom lis) (list lis)) (A B C D E F G H I) (t (append (flatten (car lis)) (flatten (cdr lis))))))
Counting the total number of atoms in a list that might contain sublists requires flattening, so we instead would do this:
(defun countallitems (lis) (cond ((null lis) 0) ((atom (car lis)) (+ 1 (countallitems (cdr lis)))) (t (+ (countallitems (car lis)) (countallitems (cdr lis))))))
Remove All
We might similarly want to remove all of an atom from the lists and sublists of a given list so again we turn to flattening:
(defun removeall (a lis) (cond ((null lis) nil) ((equal a (car lis)) (removeall a (cdr lis))) ((listp (car lis)) (cons (removeall a (car lis)) (removeall a (cdr lis)))) (t (cons (car lis) (removeall a (cdr lis)))))) (removeall 'a '(a b (a c) (d ((a) b) a) c a)) returns (B (C) (D (NIL B)) C) 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 isnt it tail recursive?
(defun fact (n) (cond ((<= n 1) 1) (t (* n (fact (- n 1))))))
The last thing fact does is *, not fact so this is not tail recursive!
Since we are guaranteed in any single recursive call that we will never need to use the parameter again in this call, we can change it
why are we guaranteed that a parameters value wont change in this call?
And the return location is always to the same location in this function Once the function terminates, it is popped off the stack and we return to the calling functions location, or the interpreter Note: we have a significant problem if an error arises and we are dropped into the debugger what is that problem?
Aside from saving on memory usage and a bit of run-time memory allocation, this optimization doesnt do anything else for us, so we dont really have to worry about tail recursion
Search Problems
Lisp was the primary language for AI research
Many AI problems revolve around searching for an answer
Consider chess you have to make a move, what move do you make? A computer program must search from all the possibilities to decide what move to make
but you dont want to search by just looking 1 move ahead if you look 2 moves ahead, you dont have twice as many possible moves, but the number of possible moves2 if you look 3 moves ahead, number of possible moves3 this can quickly get out of hand
So we limit our search by evaluating a top-level move using a heuristic function If the function says dont make this move, we dont consider it and dont search any further along that path If the function says possibly a good move, then we recursively search
by using recursion, we can backup and try another route if needed, this is known as backtracking