Skip to main content

Deep recursion

Due
Monday, 22 April 2019
Summary
We consider how to deal with recursion over lists when each list may contain lists.

Introduction: Setting the stage

As we’ve started to write procedures like tally and select, some students have started to ask “What happens when the value we’re tallying (or selecting) falls in a list within a list?”

For example, consider a procedure tally-odd, which is supposed to count the number of odd numbers in a list. What should we return for the following list?

> (tally-odd '(1 2 (3 4 5) 6 7 (8 (9 10))))

In the traditional implementation of tally-odd, the answer would be “2”. Why? Because the list contains four numbers (1, 2, 6, and 7), only two of which are odd. What about the other numbers? Because those are in lists, we normally don’t count them, with the argument that “the list contains four numbers and two lists”. In case you’ve forgotten, here is one traditional implementation.

;;; Procedure:
;;;   tally-odd
;;; Parameters:
;;;   lst, a list
;;; Purpose:
;;;   Count how many odd numbers appear in the list.
;;; Produces:
;;;   tally, an integer
;;; Preconditions:
;;;   [No additional]
;;; Postconditions:
;;;   There are exactly tally different indicies, i, such that 
;;;   (odd? (list-ref lst i)) holds (that is, returns true rather
;;;   than returning false or issuing an error.
(define tally-odd
  (lambda (lst)
    (cond
      [(null? lst)
       0]
      [(and (integer? (car lst)) (odd? (car lst)))
       (+ 1 (tally-odd (cdr lst)))]
      [else
       (tally-odd (cdr lst))])))

Here’s a slightly more concise version that some people prefer.

(define tally-odd
  (lambda (lst)
    (if (null? lst)
        0
        (+ (if (and (integer? (car lst)) (odd? (car lst))) 1 0)
           (tally-odd (cdr lst))))))

Here’s a version that uses tail recursion.

(define tally-odd
  (lambda (lst)
    (let kernel ([tally-so-far 0] 
                 [remaining lst])
      (if (null? remaining)
          tally-so-far
          (kernel (+ (if (and (integer? (car remaining)) (odd? (car remaining))) 
                         1 
                         0)
                     tally-so-far)
                  (cdr remaining))))))

Looking within nested lists

What if we wanted to count values within nested lists? We probably need one additional condition for the case in which the car of the list is itself a list. What should we do in that case? Probably tally within that list and then continue with the rest of the list. Here’s one way to express that idea, using the first version of tally-odd.

;;; Procedure:
;;;   deep-tally-odd
;;; Parameters:
;;;   lst, a list
;;; Purpose:
;;;   Count how many odd numbers appear in the list or any of its
;;;   component lists..
;;; Produces:
;;;   tally, an integer
(define deep-tally-odd
  (lambda (lst)
    (cond
      [(null? lst)
       0]
      [(and (integer? (car lst)) (odd? (car lst)))
       (+ 1 (deep-tally-odd (cdr lst)))]
      [(list? (car lst))
       (+ (tally-odd (car lst)) (deep-tally-odd (cdr lst)))]
      [else
       (deep-tally-odd (cdr lst))])))

Let’s see how well that works.

> (deep-tally-odd '(1 2 (3 4 5) 6 7 (8 (9 10)) 11))
5

Hmmmm. That’s interesting. Why did it count five odd numbers when there seem to be six? Can you determine the reason? Let’s trace it.

(deep-tally-odd '(1 2 (3 4 5) 6 7 (8 (9 10)) 11))
=> (+ 1 (deep-tally-odd '(2 (3 4 5) 6 7 (8 (9 10)) 11)))
=> (+ 1 (deep-tally-odd '((3 4 5) 6 7 (8 (9 10)) 11)))
=> (+ 1 (+ (tally-odd '(3 4 5)) (deep-tally-odd '(6 7 (8 (9 10)) 11))))
=> (+ 1 (+ (+ 1 (tally-odd '(4 5))) (deep-tally-odd '(6 7 (8 (9 10)) 11))))
=> (+ 1 (+ (+ 1 (tally-odd '(5))) (deep-tally-odd '(6 7 (8 (9 10)) 11))))
=> (+ 1 (+ (+ 1 (+ 1 (tally-odd '()))) (deep-tally-odd '(6 7 (8 (9 10)) 11))))
=> (+ 1 (+ (+ 1 (+ 1 0)) (deep-tally-odd '(6 7 (8 (9 10)) 11))))
=> (+ 1 (+ (+ 1 1) (deep-tally-odd '(6 7 (8 (9 10)) 11))))
=> (+ 1 (+ 2 (deep-tally-odd '(6 7 (8 (9 10)) 11))))

It looks like things are going well so far. But we’ll keep going. There’s a 6 at the front of the list, which is not an odd integer, so deep-tally-odd skips over it.

=> (+ 1 (+ 2 (deep-tally-odd '(7 (8 (9 10)) 11))))
=> (+ 1 (+ 2 (+ 1 (deep-tally-odd '((8 (9 10)) 11)))))
=> (+ 1 (+ 2 (+ 1 (+ (tally-odd '(8 (9 10))) (deep-tally-odd '(11))))))
=> (+ 1 (+ 2 (+ 1 (+ (tally-odd '((9 10))) (deep-tally-odd '(11))))))

What happens now? tally-odd looks at the car of '((9 10)), which is '(9 10). It’s not an odd integer. So tally-odd skips over the '(9 10).

=> (+ 1 (+ 2 (+ 1 (+ (tally-odd '()) (deep-tally-odd '(11))))))
=> (+ 1 (+ 2 (+ 1 (+ 0 (deep-tally-odd '(11))))))
=> (+ 1 (+ 2 (+ 1 (+ 0 (+ 1 (deep-tally-odd '()))))))
=> (+ 1 (+ 2 (+ 1 (+ 0 (+ 1 0)))))
=> (+ 1 (+ 2 (+ 1 (+ 0 1))))
=> (+ 1 (+ 2 (+ 1 1)))
=> (+ 1 (+ 2 2))
=> (+ 1 4)
=> 5

It appears that we missed the 9 because we did not look deeply within the list '(8 (9 10)). What’s the solution? We replace the call to tally-odd with one to deep-tally-odd.

(define deep-tally-odd
  (lambda (lst)
    (cond
      [(null? lst)
       0]
      [(and (integer? (car lst)) (odd? (car lst)))
       (+ 1 (deep-tally-odd (cdr lst)))]
      [(list? (car lst))
       (+ (deep-tally-odd (car lst)) (deep-tally-odd (cdr lst)))]
      [else
       (deep-tally-odd (cdr lst))])))

Let’s see how the new one works.

> (deep-tally-odd '(1 2 (3 4 5) 6 7 (8 (9 10)) 11))
6
> (deep-tally-odd '(((((((1) 2 3 4) 5) 6) (7 8 9) 10) 11) 12 13))
7
> (deep-tally-odd '(((((1))))))
1

This version seems to work well enough.

Why do we call it deep-tally-odd? Because the traditional term for recursion that goes into sublists is “deep recursion”; you can think about it going “deep” into the nested list structure.

Generalizing the solution

You may recall that we like to work with generalized solutions, finding patterns in our code. We’ll start with one for tallying any type of value (or at least any non-list type of value).

(define deep-tally
  (lambda (lst pred?)
    (cond
      [(null? lst)
       0]
      [(pred? (car lst))
       (+ 1 (deep-tally (cdr lst) pred?))]
      [(list? (car lst))
       (+ (deep-tally (car lst) pred?) (deep-tally (cdr lst) pred?))]
      [else
       (deep-tally (cdr lst) pred?)])))

Let’s do a quick check.

> (deep-tally '(((((((1) 2 3 4) 5) 6) (7 8 9) 10) 11) 12 13)
              (conjoin integer? odd?))
7
> (deep-tally '(((((((1) 2 3 4) 5) 6) (7 8 9) 10) 11) 12 13)
              (conjoin real? (section < <> 5)))
4

Seems good. In addition to parameterizing procedures to generalize, we sometimes look more broadly at overall patterns of recursion. Here’s one that simplifies and generalize the form of deep-tally-odd.

(define DEEP-FUN
  (lambda (lst)
    (cond
      [(BASE-CASE-TEST? lst)
       (BASE-CASE-COMPUTATION lst)]
      [(list? (car lst))
       (COMBINE1 (DEEP-FUN (car lst)) (DEEP-FUN (cdr lst)))]
      [else
       (COMBINE2 (car lst) (DEEP-FUN (cdr lst)))])))

Self checks

Check 1: Deep tallying, revisited

What result do you expect to get for the following procedure call?

> (deep-tally '(((((((1) 2 3 4) 5) 6) (7 8 9) 10) 11) 12 13)
              pair?)

Check 2: deep-tally-odd, revisited

Rewrite deep-tally-odd using the DEEP-FUN pattern.

Acknowledgements

This reading was newly written for spring 2019.