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 link "reading on procedures as values procedures-as-values.xhtml], making the base-case value and the combiner operation into parameters, we arrive at the following definition:
;;; fold-list: general pattern for list recursion ;;; Givens: ;;; BASE-CASE-VALUE, a value. ;;; COMBINER, a binary procedure. ;;; Result: ;;; RECURRER, a unary procedure. ;;; Preconditions: ;;; (1) BASE-CASE-VALUE satisfies the preconditions that ;;; COMBINER imposes on its second argument. ;;; (2) Any value returned by COMBINER satisfies the ;;; preconditions that COMBINER imposes on its second ;;; argument. ;;; Postcondition: ;;; Given a list LS of values that satisfy the preconditions ;;; that COMBINER imposes on its first argument, RECURRER ;;; returns BASE-CASE-VALUE if LS is empty, and otherwise ;;; returns the result of applying COMBINER to (1) the car ;;; of LS and (2) the result of applying RECURRER recursively ;;; to the cdr of LS. (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 '() (lambda (new recursive-result)
(cons (* new new) recursive-result))))
(define lengths
(fold-list '() (lambda (new recursive-result)
(cons (length new) recursive-result))))
(define filter-out-negatives
(fold-list '() (lambda (new recursive-result)
(if (negative? new)
recursive-result
(cons new recursive-result)))))
(define filter-out-skips
(fold-list '() (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 unriffle
(fold-list (list '() '())
(lambda (new recursive-result)
(list (cons new (cadr recursive-result))
(car recursive-result)))))
(define intersection
(lambda (left right)
((fold-list '() (lambda (new recursive-result)
(if (member new right)
(cons new recursive-result)
recursive-result)))
left)))
(define remove
(lambda (predicate)
(fold-list '() (lambda (new recursive-result)
(if (predicate new)
recursive-result
(cons new recursive-result))))))
(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 '() (lambda (new recursive-result)
(cons (car new) recursive-result))))
We can also abstract out the general pattern of tail recursion on lists:
;;; tail-fold-list: general pattern of tail recursion on lists ;;; Givens: ;;; BASE-CASE-VALUE, a value. ;;; COMBINER, a binary procedure. ;;; Result: ;;; FOLDER, a unary procedure. ;;; Precondition: ;;; Any value returned by COMBINER satisfies the preconditions ;;; that COMBINER imposes on its second argument. ;;; Postcondition: ;;; The behavior of FOLDER is specified in terms of a related ;;; binary procedure, KERNEL. Given a list LS of values that ;;; satisfy the preconditions that COMBINER imposes on its ;;; first argument, FOLDER returns the same value that KERNEL ;;; returns when given LS and BASE-CASE-VALUE as its arguments. ;;; Given arguments REST and SO-FAR, KERNEL returns SO-FAR if ;;; REST is empty; otherwise, it returns the same values that ;;; it would return if given the cdr of REST as its first ;;; argument and, as its second argument, the result of applying ;;; COMBINER to the car of LS and SO-FAR. (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 '() 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.)
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:
;;; repeat: apply a procedure some given number of times ;;; in succession, starting with a given value ;;; Givens: ;;; BASE-CASE-VALUE, a value. ;;; TRANSFORMER, a unary procedure. ;;; Result: ;;; RECURRER, a unary procedure. ;;; Preconditions: ;;; (1) BASE-CASE-VALUE satisfies the preconditions that ;;; TRANSFORMER imposes on its argument. ;;; (2) Any value returned by TRANSFORMER satisfies the ;;; preconditions that TRANSFORMER imposes on its ;;; argument. ;;; Postconditions: ;;; Given a natural number NUMBER, RECURRER returns ;;; BASE-CASE-VALUE if NUMBER is zero, and otherwise ;;; returns the result of applying TRANSFORMER to ;;; the result of applying RECURRER to the predecessor ;;; of NUMBER. (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
integers that illustrate the use of
this procedure. (The left-section procedure was defined in the reading on
procedures as values.)
(define power-of-two (repeat 1 double))
(define list-fill
(lambda (item len)
((repeat '() (left-section cons item)) len)))
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:
;;; fold-natural: general pattern for recursion on natural ;;; numbers ;;; Givens: ;;; BASE-CASE-VALUE, a value. ;;; COMBINER, a binary procedure. ;;; Result: ;;; RECURRER, a unary procedure. ;;; Preconditions: ;;; (1) BASE-CASE-VALUE satisfies the preconditions that ;;; COMBINER imposes on its second argument. ;;; (2) Any value returned by COMBINER satisfies the ;;; preconditions that COMBINER imposes on its ;;; second argument. ;;; Postconditions: ;;; Given a natural number NUMBER, RECURRER returns ;;; BASE-CASE-VALUE if NUMBER is zero, and otherwise ;;; returns the result of applying COMBINER to (1) ;;; NUMBER and (2) the result of applying RECURRER to ;;; the predecessor of NUMBER. (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:
;;; tail-fold-natural: general pattern for tail recursion ;;; on natural numbers ;;; Givens: ;;; BASE-CASE-VALUE, a value. ;;; COMBINER, a binary procedure. ;;; Result: ;;; FOLDER, a unary procedure. ;;; Preconditions: ;;; (1) BASE-CASE-VALUE satisfies the preconditions that ;;; COMBINER imposes on its second argument. ;;; (2) Any value returned by COMBINER satisfies the ;;; preconditions that COMBINER imposes on its ;;; second argument. ;;; Postcondition: ;;; The behavior of FOLDER is specified in terms of a related ;;; binary procedure, KERNEL. Given a a natural number NUMBER, ;;; FOLDER returns the same value that KERNEL returns when ;;; given NUMBER and BASE-CASE-VALUE as its arguments. ;;; Given arguments REMAINING and SO-FAR, KERNEL returns ;;; SO-FAR if REMAINING is zero; otherwise, it returns the same ;;; values that it would return if given the predecessor of ;;; REMAINING as its first argument and, as its second argument, ;;; the result of applying COMBINER to REMAINING and SO-FAR. (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)))))))
I am indebted to Professor Ben Gum for his contributions to the development of this reading.