COM1340 - Winter 2003
Tree recursion on lists
January 29, 2003

In the procedures operating on lists that we've seen so far, recursive calls have worked just on the cdrs of lists. Think of the my-length procedure that we saw earlier as a simple example. Such procedures traverse just the ``spines'' of their input lists, though the input may contain deeply-nested lists. Consider the operation of list-length on
 '(a (((b))) (((((((c))))))) d)

The time required to calculate the length of this list is the same as for any 4-element list, because the nesting structure is irrelevant to that calculation.

With so-called flat recursion, procedures have the form

 (define (proc lst)
   (if (null? lst)
      ... do something with the empty list ...
      ; otherwise
      ... somehow combine (car lst) with (proc (cdr lst)) ...

What's explicitly missing from this formulation is a recursive call on the car of the input -- even though it may itself be a list.

Example: Find an atom anywhere in a list

 ; assumes lists and atoms only data in Scheme
 ; that's not true, but we'll pretend
 (define (atom? x)
   (not (or (null? x)
            (pair? x))))

 ; returns #t if atom `atm' anywhere
 ;  in possibly-nested list, #f otherwise
 (define (member* atm lst)
   (cond
     [(null? lst) #f]
     [(atom? (car lst))
      (or (eq? atm (car lst))
          (member* atm (cdr lst)))]
     [else ; car must be list
      (or (member* atm (car lst))
          (member* atm (cdr lst)))]))

Example:

  (member* 'a '(b a c)) => #t
  (member* 'a '((b) (((a c)))) => #t
  (member* 'a '((b) (((d c)))) => #f

Scheme does have a primitive named list?, which we could use in place of atom?. But it's generally a bad idea to use list? -- why?

Note that in member*, there are several recursive calls. We need them both -- why? How much of the list does this procedure traverse? Does it ever traverse the entire list? An ``extensionally-equivalent'' procedure is:

 (define (member*2 atm lst)
   (if (null? lst) 
     #f
     (let ([cdr-result (member*2 atm (cdr lst))])
       (if (atom? (car lst))
         (or (eq? atm (car lst))
             cdr-result)
         (or (member*2 atm (car lst))
             cdr-result)))))

The let form creates a local binding that is visible in the let body. A let is useful for performing a computation whose result may be used more than once. Which version should we prefer?

Example: Substitute for an atom anywhere in a list

 ; substitutes `new' for atom satisfying `pred?' anywhere in 
 ;  possibly-nested input list
 ; nesting structure of input list is preserved in output
 (define (subst* new lst)
   (if (null? lst) 
       null
       (let ([cdr-result (subst* new (cdr lst))])
         (if (atom? (car lst))
           (if (pred? (car lst))
             (cons new cdr-result)
             (cons (car lst) cdr-result))
           (cons (subst* new (car lst))
                 cdr-result)))))

Last modified: Wed, Jan 29, 2003, 3:13 pm US/Eastern
HTML conversion by TeX2page 4p4k3