Folding

Folding over lists

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:


Exercise 1

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"

Exercise 2

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.)


Exercise 3

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))))))

Folding over natural numbers

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 4

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.


Exercise 5

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)))))))

Exercise 6

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.)


Exercise 7

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

John David Stone (stone@cs.grinnell.edu)