SICP. 1.3/1.3.1
2023-02-05 Sun

1.3 Formulating Abstractions with Higher-Order Procedures

Procedures, we already know, are abstractions. The cube procedure, for example, is abstracting from the particular number we are calculating the cube of:

(define (cube x) (* x x x))

Without cube, if we had, say, to compute the cube of 3, we could use (* 3 3 3). But

…our language would lack the ability to express the concept of cubing. One of the things we should demand from a powerful programming language is the ability to build abstractions by assigning names to common patterns and then to work in terms of the abstractions directly. Procedures provide this ability.

Procedures need not take numbers only as their arguments. Procedures can take other procedures. Procedures that take other procedures as arguments are called higher-order procedures. They are an even more powerful abstraction mechanism.

1.3.1 Procedures as Arguments

The following higher-order procedure is presented:

(define (sum term a next b)
(if (> a b)
    0
    (+ (term a)
       (sum term (next a) next b))))

With it, we can the express different concepts that ``share a common underlying patter''.

We can use sum to define a procedures that computes the sum of the integers from a to b; or a procedure that computes the sum of the cubes of the integers from a to b; or even the sum of the following sequence: \(\frac{1}{1 \cdot 3} + \frac{1}{5 \cdot 7} + \frac{1}{9 \cdot 11} + \dots\). The concepts of these procedures are all instances of a more general concept, the concept expressed by sum: adding the results of the application of a certain procedure (term) to those items between a and b, picked out by starting from a and stepping forward according to application of next to the current item.

Exercise 1.29

Simpson’s Rule is a more accurate method of numerical integration than the method illustrated above. Using Simpson’s Rule, the integral of a function \(f\) between a and b is approximated as

\(\frac{h}{3} (y_0 + 4y_1 + 2y_2 + 4y_3 + 2y_4 + ... + 2y_{n-2} + 2y_{n-1} + y_n)\),

where \(h = (b - a) / n\), for some even integer \(n\), and \(y_k = f(a + kh)\). (Increasing \(n\) increases the accuracy of the approximation.) Define a procedure that takes as arguments \(f\), \(a\), \(b\), and \(n\) and returns the value of the integral, computed using Simpson's Rule. Use your procedure to integrate cube between 0 and 1 (with \(n = 100\) and \(n = 1000\)), and compare the results to those of the integral procedure shown above.

Answer:

#lang sicp
(define (sum term a next b)
  (if (> a b)
      0
      (+ (term a)
         (sum term (next a) next b))))

(define (cube x) (* x x x))

(define (integral f a b n)

  (define (f-mod x)
    (define (f2 x)
      (* 2 (f x)))
    (define (f4 x)
      (* 4 (f x)))
    (cond ((= x a) (f x))
          ((= (- (/ x (/ (- b a) n)) a) n) (f x))
          ((= (remainder (- (/ x (/ (- b a) n)) a) 2) 0) (f2 x))
          (else (f4 x))))

  (define (next x)
    (+ x
       (/ (- b a) n)))

  (* (/ (/ (- b a) n) 3)
     (sum f-mod
          a
          next
          (+ a (* n (/ (- b a) n))))))

(integral cube 0 1 100);; => 1/4
(integral cube 0 1 1000);; => 1/4

Exercise 1.30

Exercise:

The sum procedure above generates a linear recursion. The procedure can be rewritten so that the sum is performed iteratively. Show how to do this by filling in the missing expressions in the following definition:

(define (sum term a next b)
  (define (iter a result)
    (if ⟨??⟩
        ⟨??⟩
        (iter ⟨??⟩ ⟨??⟩)))
  (iter ⟨??⟩ ⟨??⟩))

Answer:

(define (sum term a next b)
  (define (iter a result)
    (if (> a b)
        result
        (iter (next a) (+ (term a) result))))
  (iter a 0))

Exercise 1.31

Exercise:

  1. The sum procedure is only the simplest of a vast number of similar abstractions that can be captured as higher-order procedures.[fn: The intent of Exercise 1.31 through Exercise 1.33 is to demonstrate the expressive power that is attained by using an appropriate abstraction to consolidate many seemingly disparate operations. However, though accumulation and filtering are elegant ideas, our hands are somewhat tied in using them at this point since we do not yet have data structures to provide suitable means of combination for these abstractions. We will return to these ideas in 2.2.3 when we show how to use sequences as interfaces for combining filters and accumulators to build even more powerful abstractions. We will see there how these methods really come into their own as a powerful and elegant approach to designing programs.] Write an analogous procedure called product that returns the product of the values of a function at points over a given range. Show how to define factorial in terms of product. Also use product to compute approximations to π using the formula…

    \(\frac{\pi}{4} = \frac{2 \cdot 4 \cdot 4 \cdot 6 \cdot 6 \cdot 8 \cdot \dots}{3 \cdot 3 \cdot 5 \cdot 5 \cdot 7 \cdot 7 \cdot \dots}\)

  2. If your product procedure generates a recursive process, write one that generates an iterative process. If it generates an iterative process, write one that generates a recursive process.

Answer:

Here is product.

(define (product term a next b)
  (if (> a b)
      1
      (* (term a) (product term (next a) next b))))

Here is factorial defined in terms of product.

(define (factorial n)
  (define (inc x) (+ 1 x))
  (define (indentity x) x)
  (product identity 1 inc n))  

Here is one way of computing an approximation to \(\pi\).

(define (product* begin end)
  (define (identity x) x)
  (define (inc2 x) (+ 2 x))
  (product identity begin inc2 end))

(* (/ (* 2 (product* 4 8) (product* 4 6))
      (square (product* 3 7)))
   4.0)

My product procedure generates a recursive process (\(\Theta(n)\) in time and space). Here is a product that generates an iterative process (\(\Theta(n)\) in time, \(\Theta(1)\) in space — assuming tail call optimization).

(define (product term a next b)
  (define (iter a result)
    (if (> a b)
        result
        (iter (next a) (* (term a) result))))
  (iter a 1))

Exercise 1.32

  1. Show that sum and product (Exercise 1.31) are both special cases of a still more general notion called accumulate that combines a collection of terms, using some general accumulation function:

    (accumulate combiner null-value term a next b)
    

    Accumulate takes as arguments the same term and range specifications as sum and product, together with a combiner procedure (of two arguments) that specifies how the current term is to be combined with the accumulation of the preceding terms and a null-value that specifies what base value to use when the terms run out. Write accumulate and show how sum and product can both be defined as simple calls to accumulate.

  2. If your accumulate procedure generates a recursive process, write one that generates an iterative process. If it generates an iterative process, write one that generates a recursive process.

Answer:

By looking at the recursive version of sum and product, we can observe that only certain elements in the two bodies are different. Those elements are:

  • The first expression after the if;
  • The procedure applied in the tail call;

As far as I can tentatively see, this means that the concepts expressed sum and product belong to a more general concept.

This latter concept abstracts over the two parts of the body mentioned. The first abstraction is an abstraction over a numeric value. The second abstraction is an abstraction over a procedure.

This is the procedure I would write to express the concept sum and product are instances of:

(define (accumulate combiner null-value term a next b)
  (if (> a b)
      null-value
      (combiner (term a)
                (accumulate combiner null-value term (next a) next b))))
(sum identity 1 inc 5) ;; => 15
(accumulate + 0 identity 1 inc 5) ;; => 15

(product identity 2 inc 9) ;; => 362880
(accumulate * 1 identity 2 inc 9) ;; => 362880

My accumulate generate a recursive process. Here is an iterative version:

(define (accumulate combiner null-value term a next b)
  (define (iter a result)
    (if (> a b)
        result
        (iter (next a) (combiner (term a) result))))
  (iter a null-value))

Exercese 1.33

Exercise:

You can obtain an even more general version of accumulate (Exercise 1.32) by introducing the notion of a filter on the terms to be combined. That is, combine only those terms derived from values in the range that satisfy a specified condition. The resulting filtered-accumulate abstraction takes the same arguments as accumulate, together with an additional predicate of one argument that specifies the filter. Write filtered-accumulate as a procedure. Show how to express the following using filtered-accumulate:

  1. the sum of the squares of the prime numbers in the interval a to b (assuming that you have a prime? predicate already written)
  2. the product of all the positive integers less than \(n\) that are relatively prime to $n$j (i.e., all positive integers \(i < n\) such that \(GCD(i,n)=1\)).

Answer:

(define (filtered-accumulate combiner filter null-value term a next b)
(cond ((> a b) null-value)
      ((filter a) (combiner (term a)
                            (filtered-accumulate combiner filter null-value term (next a) next b)))
      (else (filtered-accumulate combiner filter null-value term (next a) next b))))
(define (sum-of-the-squares-of-primes a b)
  (filtered-accumulate + prime? 0 square a inc2 b))
(define (product-int-less-than-rel-prime-to n)
  (define (rel-prime-to-n x)
    (= (gcd x n)
       1))
  (filtered-accumulate * rel-prime-to-n 1 1 inc1 (- n 1)))

Please, send me an email if you have any comment.

Created with Emacs 28.2 (Org mode 9.5.5)