Deep recursion

In Scheme, it is not only possible, but commonplace, for a list to be an element of another list. One can have a list within a list within a list within a list, and so on -- there is no fixed upper bound on levels of nesting.

For instance, the list (((a b) c) d (e (f))) -- considered simply as a datum -- has three elements: ((a b) c), d, and (e (f)). The first of these elements is a list that has two elements: (a b) and c. The list (a b) has two elements, a and b. And so on.

If you count the symbols in the list (((a b) c) d (e (f))) using a ``flat'' recursion over the list, you find that only one of the elements of that list is a symbol:

(define count-top-level-symbols
  (lambda (ls)
    (cond ((null? ls) 0)
          ((symbol? (car ls)) (+ 1 (count-top-level-symbols (cdr ls))))
          (else (count-top-level-symbols (cdr ls))))))

> (count-top-level-symbols '(((a b) c) d (e (f))))
1

The recursion does not attempt to unpack the contents of any of the elements of ls as it examines them. Since ((a b) c) is not itself a symbol, it contributes nothing to the total computed by count-top-level-symbols.

Suppose, however, that we want to write a procedure named count-all-symbols that will be able to determine that there are six symbols altogether within the datum (((a b) c) d (e (f))) -- a, b, c, d, e, and f. We'll need a different pattern of recursion for this, one that reflects our interest in the internal structure of list elements. The textbook's name for this new pattern is deep recursion.

In deep recursion, whenever we examine a list element, we first consider the possibility that that element is itself a list. If it is, we write a recursive procedure call, with the first element as its argument, in addition to the usual recursive procedure call, which takes the rest of the list as its argument. Contrast the preceding definition of count-top-level-symbols with the following definition of count-all-symbols:

(define count-all-symbols
  (lambda (ls)
    (cond ((null? ls) 0)
          ((list? (car ls))
           (+ (count-all-symbols (car ls)) (count-all-symbols (cdr ls))))
          ((symbol? (car ls)) (+ 1 (count-all-symbols (cdr ls))))
          (else (count-all-symbols (cdr ls))))))

> (count-all-symbols '(((a b) c) d (e (f))))
6

In the definition of count-all-symbols, the second cond-clause, which is new, comes into play when the first element of ls is itself a list. The recursive call (count-all-symbols (car ls)) counts the symbols that occur inside that first element, while (count-all-symbols (cdr ls)) counts the symbols that occur inside all of the remaining elements of ls (at any level). The total number of symbols in ls is found by adding the two counts.

The characteristic signs of deep recursion are (1) the insertion of the new cond-clause to detect the case in which the first element of a list is itself a list, and (2) the dual recursive procedure calls, one to deal with the car and the other with the cdr of the given list.


Exercise 1

Define a procedure count-this-symbol that takes two arguments, the first a list and the second a symbol, and computes and returns the number of occurrences of the specified symbol anywhere inside the given list (including nested lists). (Hint: use count-all-symbols as a pattern.)


Exercise 2

Let's use the term ``tree of symbols'' for a datum like the one used in the preceding examples -- specifically, a list in which each element is either a symbol or another tree of symbols. Define a predicate tree-of-symbols? that takes one argument and returns #t if the argument is a tree of symbols, #f if it is not. (Such a predicate would be useful in adding a precondition test to count-this-symbol.)


Exercise 3

Define a procedure sum-all that takes a tree of numbers -- a list in which each element is either a number or another tree of numbers -- and determines the sum of all the numbers in the tree.


On page 101 of the textbook, the authors introduce the term nesting level for the number of nested lists within which a datum is enclosed. The depth of a tree of symbols is the maximum nesting level of any of the symbols that occur in it. Here is a procedure that computes the depth of a tree of symbols:

(define depth
  (lambda (tr)
    (cond ((null? tr) 0)
          ((list? (car tr))
           (max (+ 1 (depth (car tr))) (depth (cdr tr))))
          ((symbol? (car tr)) (max 1 (depth (cdr tr))))
          (else 0))))

Exercise 4

What is the depth of the datum (((a b) c) d (e (f)))? Why?


Exercise 5

Give an example of a tree of symbols of depth 7. Have DrScheme check your answer.


Exercise 6

Define a procedure depth-tally that takes two arguments, a tree of symbols tr and a positive integer level, and counts how many symbols occur inside tr at nesting level level exactly. (For example, in the tree of symbols (((a b) c) d (e (f))), the nesting level of the symbols c and e is 2, and the rest of the symbols have other nesting levels; so (depth-tally '(((a b) c) d (e (f))) 2) should yield 2.)


This document is available on the World Wide Web as

http://www.cs.grinnell.edu/~stone/courses/scheme/deep-recursion.xhtml

created February 13, 1997
last revised April 20, 2000

Henry Walker (walker@cs.grinnell.edu) and John David Stone (stone@cs.grinnell.edu)