SICP Study

1.3 Formulating Abstractions with Higher-Order Procedures

We have seen that procedures are, in effect, abstractions that describe compound operations on numbers independent of the particular numbers. (Section 1.3)

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. (Section 1.3)

1.3.1 Procedures as Arguments

Procedures that compute a sum all look the same:

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

This is a useful abstraction, just as sigma notation is useful in math because the summation of a series is so common.

The power of sigma notation is that it allows mathematicians to deal with the concept of summation itself rather than only with particular sums. (Section 1.3.1)

1.3.2 Constructing Procedures Using Lambda

lambda creates anonymous procedures. They are just like the procedures created by define, but without a name: (lambda (formal-parameters) body).

A lambda expression can be used as the operand in a combination. It will be evaluated to a procedure and applied to the arguments (the evaluated operands). The name comes from the λ-calculus, a formal system invented by Alonzo Church.

Using let to create local variables

We often need local variables in addition to the formal parameters. We can do this with a lambda expression that takes the local variables as arguments, but this is so common that there is a special form let to make it easier.

The general form of a let-expression is:

(let ((var1 exp1)
      (var2 exp2)
      ...
      (varn expn))
  body)

This is just syntactic sugar for:

((lambda (var1 var2 ... varn)
   body)
 exp1
 exp2
 ...
 expn)

1.3.3 Procedures as General Methods

So far, we have seen:

Now we will take it a bit further.

Finding roots of equations by the half-interval method

(define (search f neg-point pos-point)
  (let ((midpoint (average neg-point pos-point)))
    (if (close-enough? neg-point pos-point)
        midpoint
        (let ((test-value (f midpoint)))
          (cond ((positive? test-value)
                 (search f neg-point midpoint))
                ((negative? test-value)
                 (search f midpoint pos-point))
                (else midpoint))))))

(define (close-enough? x y)
  (< (abs (- x y)) 0.001))

(define (half-interval-method f a b)
  (let ((a-value (f a))
        (b-value (f b)))
    (cond ((and (negative? a-value) (positive? b-value))
           (search f a b))
          ((and (negative? b-value) (positive? a-value))
           (search f b a))
          (else
           (error "values are not of opposite sign" a b)))))

We can use half-interval-method to approximate π\pi as a root of sinx=0:\sin x = 0\htmlClass{math-punctuation}{\text{:}}

(half-interval-method sin 2.0 4.0)
→ 3.14111328125

Finding fixed points of functions

(define tolerance 0.00001)
(define (fixed-point f first-guess)
  (define (close-enough? v1 v2)
    (< (abs (- v1 v2)) tolerance))
  (define (try guess)
    (let ((next (f guess)))
      (if (close-enough? guess next)
          next
          (try next))))
  (try first-guess))

We can use fixed-point to approximate the fixed point of the cosine function:

(fixed-point cos 1.0)
→ 0.7390822985224023

1.3.4 Procedures as Returned Values

Passing procedures as arguments gives us expressive power. Returning procedures from functions gives us even more. For example, we can write a procedure that creates a new procedure with average damping:

(define (average-damp f)
  (lambda (x) (average x (f x))))

If we use average-damp on square, we get a procedure that calculates the sum of the numbers from 1 to n:n\htmlClass{math-punctuation}{\text{:}}

((average-damp square) 10)
→ 55

(+ 1 2 3 4 5 6 7 8 9 10)
→ 55
Highlights

In general, there are many ways to formulate a process as a procedure. Experienced programmers know how to choose procedural formulations that are particularly perspicuous, and where useful elements of the process are exposed as separate entities that can be reused in other applications. (Section 1.3.4)

Newton’s method

The square-root procedure we wrote earlier was a special case of Newton’s method. Given a function f(x),f(x)\htmlClass{math-punctuation}{\text{,}} the solution to f(x)=0f(x) = 0 is given by the fixed point of

xxf(x)f(x).x ↦ x - \frac{f(x)}{f'(x)}.

Newton’s method converges very quickly—much faster than the half-interval method in favorable cases. We need a procedure to transform a function into its derivative (a new procedure). We can use a small dxdx for this:

f(x)=f(x+dx)f(x)dx.f'(x) = \frac{f(x+dx) - f(x)}{dx}.

This translates to the following procedure:

(define (deriv f)
  (lambda (x) (/ (- (f (+ x dx)) (f x)) dx)))

Now we can do things like this:

(define (cube x) (* x x x))
(define dx 0.00001)
((deriv cube) 5)
→ 75.00014999664018

Abstractions and first-class procedures