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))))))
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.
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)))])))
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?)
deep-tally-odd
, revisitedRewrite deep-tally-odd
using the DEEP-FUN
pattern.
This reading was newly written for spring 2019.