Of all the computational patterns we've looked at so far, the most common is list recursion, in which we (1) specify the value to be returned when the list is empty; (2) divide any non-empty list into its car and its cdr; (3) call the procedure we're defining recursively to deal with the cdr; and (4) combine the car of the list somehow with the result of the recursive call to obtain the final value.
Only two things vary from one instance of this pattern to another: the value that is returned in the base case, and the operation that combines the car of the list with the result of the recursive call. If we follow the method for constructing higher-order procedures that was introduced in the first lab on procedures as values, making the base-case value and the combiner operation into parameters, we arrive at the following definition:
(define fold-list
(lambda (base-case-value combiner)
(letrec ((recurrer (lambda (ls)
(if (null? ls)
base-case-value
(combiner (car ls) (recurrer (cdr ls)))))))
recurrer)))
Consider how many of the procedures that we've studied can be defined in
terms of fold-list:
(define sum (fold-list 0 +))
(define product (fold-list 1 *))
(define square-each-element
(fold-list null (lambda (new recursive-result)
(cons (* new new) recursive-result))))
(define lengths
(fold-list null (lambda (new recursive-result)
(cons (length new) recursive-result))))
(define filter-out-negatives
(fold-list null (lambda (new recursive-result)
(if (negative? new)
recursive-result
(cons new recursive-result)))))
(define filter-out-skips
(fold-list null (lambda (new recursive-result)
(if (eq? 'skip new)
recursive-result
(cons new recursive-result)))))
(define tally-occurrences
(lambda (sym ls)
((fold-list 0 (lambda (new recursive-result)
(if (eq? sym new)
recursive-result
(+ recursive-result 1))))
ls)))
(define unshuffle
(fold-list (list null null)
(lambda (new recursive-result)
(list (cons new (cadr recursive-result))
(car recursive-result)))))
(define intersection
(lambda (left right)
((fold-list null (lambda (new recursive-result)
(if (member new right)
(cons new recursive-result)
recursive-result)))
left)))
(define tallier
(lambda (predicate)
(fold-list 0 (lambda (new recursive-result)
(if (predicate new)
(+ recursive-result 1)
recursive-result)))))
(define association-list-keys
(fold-list null (lambda (new recursive-result)
(cons (car new) recursive-result))))
(define remove
(lambda (predicate)
(fold-list null (lambda (new recursive-result)
(if (predicate new)
recursive-result
(cons new recursive-result))))))
Using fold-list, define a procedure concatenate
that takes a list of strings as its argument and constructs and returns one
long string formed by appending together all of the list elements.
> (concatenate (list "alpha" "beta" "gamma" "delta")) "alphabetagammadelta"
Determine and explain the effect of the procedure mystery-1,
defined below, which takes any list as its argument.
(define mystery-1
(fold-list (list null)
(lambda (new recursive-result)
(append recursive-result
(map (left-section cons new) recursive-result)))))
We can also abstract out the general pattern of tail recursion with lists:
(define tail-fold-list
(lambda (base-case-value combiner)
(lambda (ls)
(let kernel ((rest ls)
(so-far base-case-value))
(if (null? rest)
so-far
(kernel (cdr rest) (combiner (car rest) so-far)))))))
For example, (tail-fold-list null cons) is synonymous with
reverse: Given a list, it returns a list containing the same
elements, but in the opposite order. (Each call to the kernel
procedure transfers one element from the front of rest, which
gets shorter and shorter, to the front of so-far, which gets
longer and longer.)
Determine and explain the effect of the procedure mystery-2,
defined below, which takes any list of digit characters as its argument.
(define mystery-2
(tail-fold-list 0 (lambda (new so-far)
(+ (* 10 so-far)
(- (char->integer new)
(char->integer #\0))))))
Analogous higher-order ``folding'' procedures capture the common patterns of recursion with natural numbers. In the simplest case, the natural number simply counts the number of times some procedure is applied in transforming a base-case value into the final result:
(define repeat
(lambda (base-case-value transformer)
(letrec ((recurrer (lambda (number)
(if (zero? number)
base-case-value
(transformer (recurrer (- number 1)))))))
recurrer)))
Here are two examples from the lab on recursion with natural numbers that illustrate the use of this procedure:
(define power-of-two (repeat 1 (left-section * 2)))
(define fill-list
(lambda (item len)
((repeat null (left-section cons item)) len)))
Exercise 3.9 of the textbook (p. 83) invites you to ``define a procedure
wrapa that takes as arguments an item a and a
nonnegative integer num and wraps num sets of
parentheses around the item a,'' thus:
> (wrapa 'gift 1) (gift) > (wrapa 'sandwich 2) ((sandwich)) > (wrapa 'prisoner 5) (((((prisoner))))) > (wrapa 'moon 0) moon
Use repeat to define wrapa concisely.
Using repeat, define and test a DrScheme procedure that takes
any natural number len as argument and returns a list of
length len in which each element is a random integer in the
range from 0 to 99.
In a procedure constructed by repeat, the number is used as a
pure counter, not in the transformation of the recursive result.
On the other hand, we sometimes want to incorporate the number into the
computation itself, as in the definitions of termial and
count-down:
(define termial
(lambda (number)
(if (zero? number)
0
(+ number (termial (- number 1))))))
(define count-down
(lambda (number)
(if (zero? number)
(list 0)
(cons number (count-down (- number 1))))))
We can abstract the common structure of these procedures in the
higher-order procedure fold-natural:
(define fold-natural
(lambda (base-case-value combiner)
(letrec ((recurrer (lambda (number)
(if (zero? number)
base-case-value
(combiner number (recurrer (- number 1)))))))
recurrer)))
The termial procedure is (fold-natural 0 +), and
count-down is (fold-natural (list 0) cons).
There is also a tail-recursive fold for natural numbers:
(define tail-fold-natural
(lambda (base-case-value combiner)
(lambda (number)
(let kernel ((remaining number)
(so-far base-case-value))
(if (zero? remaining)
so-far
(kernel (- remaining 1) (combiner remaining so-far)))))))
Using fold-natural, define and test a procedure
harmonic that takes any natural number n and
returns the nth harmonic number, which is the sum of
the reciprocals of the positive integers less than or equal to
n. (For instance, the fourth harmonic number is 1/1 + 1/2 +
1/3 + 1/4, or 25/12.)
Determine and explain the effect of the procedure mystery-3,
defined below, which takes any string as its argument.
(define mystery-3
(lambda (str)
((tail-fold-natural null (lambda (new so-far)
(cons (string-ref str (- new 1))
so-far)))
(string-length str))))
created March 14, 2000
last revised March 21, 2000