When writing recursive functions, pay attention to the type of what you are recurring on, as well as to the type of what you want your result to be. That information alone can help you determine the pieces to use in writing the function. As an example, consider the following simple recursive design pattern.
(define (fn arg) (if (basecase? arg) basecaseanswer (combine (f arg) (fn (smaller arg)))))
Here are four examples of functions that follow this design pattern. Notice what they take as input and output.
;;; takes a number n as input and returns a number that is equal to n! (define (fact n) (if (zero? n) 1 (* n (fact (1- n))))) ;;; takes a number as input and returns a list of numbers ;;; that looks like a shuttle launching countdown (define (make-lst n) (if (zero? n) (list 'blastoff) (cons n (make-lst (1- n)))))
;;; take a list as input and returns a number equal to the length of the list (define (count lst) (if (null? lst) 0 (+ 1 (count (cdr lst))))) ;;; takes a list as input and returns a list which is just a ;;; copy of the original list (define (copy lst) (if (null? lst) '() (cons (car lst) (copy (cdr lst)))))
Just from the type of the input, you can narrow down the function that you use to determine whether or not you've hit the basecase to predicates that recognize objects of that same type. The input type call also help you determine what is often called the natural recursion of the function, which is how to break down the function into its recursive call.
|fn||type of arg||basecase?||smaller||f|
Note that the function f in the design pattern is some arbitrary function, however if it is the identity function instead of writing (identity arg) in the code we just write arg.
For functions that take numbers as input, the functions basecase? and smaller are functions that act on numbers, and similarly with lists. There are often not so many functions that recognize any one particular type, and not so many ways to recurse on any one data type, so this drastically reduces your options to choose from for those functions.
The type of the output of the function can also give you some clues about the other components of your recursive funtion.
|fn||type of output||combine||basecaseanswer|
Usually basecaseanswer tends to be a constant, although in general it might actually depend on arg. When returning numbers, you usually use 0 as the basecaseanswer and + as the combining function or 1 and *. When returning lists, the basecaseanswer is frequently the empty list and the combining function is almost always cons.
Again, notice that the type we want our output to be determines not only the type of the basecase answer, but also severely limits our options of functions to use to combine the result of the recursive call with some local value.
Of course, this generalizes to other design patterns as well. As an exercise, see what information you can glean from the following two functions, one which has more than one input argument but still uses a design pattern similar to the one given here, and one which uses the tail-recursive design pattern.
;;; takes two lists as input and returns a list equal to the concatenation ;;; of the two lists. It recurs over the first list, making a copy of it, ;;; but instead of the empty list at the end it puts the (actual) second list (define (append a b) (if (null? a) b (cons (car a) (append (cdr a) b))))
;;; takes a list as input, returns a list that is the reverse of the original (define (reverse x) (reverse-h x '())) (define (reverse-h x ans) (if (null? x) ans (reverse-h (cdr x) (cons (car x) ans))))