SICP. Foreword–1.1.
2022-11-02 Wed

Memorably, computer science — the subject matter of the book — is presented as a non-science whose signficance has little to do with computers (see also the lecture). Just as memorably, computer science is presented as a branch of epistemology, that is, the study of knowledge (which, tipically, is considered part of philosophy). In particular, computer science is procedural epistemology. We deal here with the systematization of imperative, how-to, knowledge, as opposed to ``declarative'' knowledge. The computer scientist deals, indeed, with a complexity that resembles that of the human mind — notice the epigraph from Locke. Much of his job is dominating the ``intellectual complexity'' of software systems.

Here is a bullet-point summary starting from the foreword up to section 1.1 included, and my solutions of the exercises. As expected, these first pages present fundamental concepts and building blocks. I'm using Emacs Lisp, for the moment. I'll probably switch to Racket at some point.

Foreword (by Alan J. Perlis)

  • The subject matter of this book involves three foci of phenomena
    • the human mind;
    • collection of computer programs;
    • the computer.
  • Idioms: ``standard program structures of whose corrected we have becamse sure''.
  • Algorithms: programs that ``perform a precise mathematical function such as sorting or finding the maximum of a sequence of numbers , determining primality, or finding the square root.''

A programmer should acquire good algorithms and idioms. (xiii)

Preface to the First Edition

  • The authors express two major concerns:
    1. They want to establish the idea ``idea that a computer language is not just a way of getting a computer to perform operations but rather that it is a novel formal medium for expressing ideas about methodology'' (my emphasis).
    2. The essential material of interest here are the techniques used to control the intellectual complexity of large software systems; not: matters of syntax of particular languages, clever algorithms in special contexts, mathematical analysis of algorithms, or foundations of computing.
  • Computer science is not science and its significance has very little to do with computers.
  • Procedural Epistemology: ``the study of the structure of knowledge from an imperative point of view, as opposed to a more declarative point of view…''.

1. Building Abstractions with Procedures

  • A Computational process is an abstract thing that inhabits a computer.
  • Data are other abstract things manipulated by processes.
  • A program is a pattern of rules that direct the evolution of a process.
  • Programs are composed from expressions in programming languages.
  • Good design is modular.
  • Lisp interpreter: ``a machine that carries out processes described in the Lisp language''.
  • ``…Lisp descriptions of processes, called procedures, can themselves be represented and manipulated as Lisp data. The importance of this is that there are powerful program-design techniques that rely on the ability to blur the traditional distinction between ``passive'' data and ``active'' processes.''

1.1 The elements of programming

  • A powerful programming language enables us to describe processes and organize our ideas about them, not merely to instruct a computer to perform some operations.
  • To organize our ideas about processes well we must be able to combine simple ideas to give life to more complex ones. We can do so through three mechanisms provided by a powerful programming language:
    • Primitive expressions
    • Means of combinations
    • Means of abstractions

Expressions

  • ``You type an expression, and the interpreter responds by displaying the result of its evaluating that expression.''
  • Combinations;
  • Operator;
  • Operands;
  • Arguments.
  • read-eval-print loop: ``the intepreter always operates in the same basic cycle: It reads an expression from the terminal, evaluates the expression, and prints the result''.

Naming and the Environment

  • A programming language provides means to use names to refer to computational object. ``We say that the name identifies a variable whose value is the object.''
  • `` Define is our language's simples means of abstraction.
  • Environment.

Evaluating Combinations

  • The interpreter evaluates combinations by following a procedure:

    • To evaluate a combination, do the following:
      1. Evaluate the subexpressions of the combination;
      2. Apply the procedure that is the value of the leftmost subexpression (the operator) to the arguments that are the values of the other subexpressions (the operands).
  • The evaluation rule is recursive.
  • We can view the evaluation in terms of a tree (See p. 10).

    figure1-1.gif

  • Tree accumulation.
  • Special forms constitute exceptions to the general evaluation rule. Define is a special form.

Compound Procedures

  • Procedure definitions: a much more powerful abstraction technique.
  • Compound procedures.

The Substitution Model for Procedure Application

  • Application process for compound procedures:
    • To apply a compound procedure to arguments, evaluate the body of the procedure with each formal parameter replaced by the corresponding argument.
  • Substitution model for procedure application: a model to determine the ``meaning'' of procedure application (in this chapter);
    • This is not how interpreters actually work;
    • The substitution model is the first of a sequence of increasingly elaborated models presented in this book of how intepreters work. Chapter 5 will present a complete implemetation of an interpreter an compiler.
  • Lisp uses applicative-order evaluation: the interpreter evaluates the operator and the operands and then applies the resulting procedure to the resulting arguments — the procedure (followed by the intepreter to evaluate a combination) described above.
  • An alternative evaluation mode: normal-order evaluation.

Conditional Expressions and Predicates

  • cond
  • if
  • and
  • or
  • not

Exercise 1.1

Exercise:

Below is a sequence of expressions. What is the result printed by the interpreter in response to each expression? Assume that the sequence is to be evaluated in the order in which it is presented.

10
(+ 5 3 4)
(- 9 1)
(/ 6 2)
(+ (* 2 4) (- 4 6))
(define a 3)
(define b (+ a 1))
(+ a b (* a b))
(= a b)
(if (and (> b a) (< b (* a b)))
    b
    a)
(cond ((= a 4) 6)
      ((= b 4) (+ 6 7 a))
      (else 25))
(+ 2 (if (> b a) b a))
(* (cond ((> a b) a)
         ((< a b) b)
         (else -1))
   (+ a 1))

Answer:

10
;; 10

(+ 5 3 4)
;; 12

(- 9 1)
;; 8

(/ 6 2)
;; 3

(+ (* 2 4) (- 4 6))
;; 6

The authors, p. 8 fn. 8, say that the response to evaluating definitions is ``highly implementation-dependent''.

I gather that Scheme's define, when used for variables, is equivalent for Elisp's setq.

(setq a 3)
;; 3

(setq b (+ a 1))
;; 4

(+ a b (* a b))
;; 19

I gather that Scheme's =, when used for variables, is equivalent for Elisp's eq.

(eq a b)
;; nil

(if (and (> b a) (< b (* a b)))
    b
  a)
;; 4

(cond ((= a 4) 6)
      ((= b 4) (+ 6 7 a))
      (else 25))
;; 16

(+ 2 (if (> b a) b a))
;; 6

(* (cond ((> a b) a)
         ((< a b) b)
         (else -1))
   (+ a 1))
;; 16

Exercise 1.2

Exercise:

Translate the following expression into prefix form

\(\frac{5 + 4 + (2 - (3 - (6 + \frac{4}{5})))}{3(6 - 2)(2 - 7)}\)

Answer:

(/ (+ 5 
      4 
      (- 2
         (- 3
            (+ 6
               (/ 4 5))))) 
   (* 3 
      (- 6 2) 
      (- 2 7))) 

Exercise 1.3

Exercise:

Define a procedure that takes three numbers as arguments and returns the sum of the squares of the two larger numbers.

Answer:

This was my first solution:

(defun foo (a b c)
  (+ (square (if (> a b) a b))
     (square (if (> c
                    (if (> a b) b a))
                    c
                    (if (> a b) b a)))))

That works, although it is not ideal, because the combination (> a b) is evaluated three times…

Exercise 1.4

Exercise:

Observe that our model of evaluation allows for combinations whose operators are compound expressions. Use this observation to describe the behavior of the following procedure:

(define (a-plus-abs-b a b)
  ((if (> b 0) + -) a b))

Answer:

Behavior: If b is greater than 0, then apply + to a and b, that is, add b to a. Otherwise, apply - to a and b, that is, to subtract b from a.

But subtracting a negative number, means adding it! So, behavior: Add the absolute value of b to a.

Exercise 1.5

Exercise:

Ben Bitdiddle has invented a test to determine whether the interpreter he is faced with is using applicative-order evaluation or normal-order evaluation. He defines the following two procedures:

(define (p) (p))

(define (test x y)
  (if (= x 0)
      0
      y))

Then he evaluates the expression

(test 0 (p))

What behavior will Ben observe with an interpreter that uses applicative-order evaluation? What behavior will he observe with an interpreter that uses normal-order evaluation? Explain your answer. (Assume that the evaluation rule for the special form if is the same whether the interpreter is using normal or applicative order: The predicate expression is evaluated first, and the result determines whether to evaluate the consequent or the alternative expression.)

Answer:

In the case of applicative-order evaluation, ``the interpreter first evaluates the operator and operands and then applies the resulting procedure to the resulting arguments'' (p. 16). This means that the interpreter will evaluate test, then 0 and then (p). test evaluates to a procedure. 0 evaluates to 0. But (p) evaluates to (p), which evaluates to (p), which evaluates to (p)', which… ad infinitum. So, the interpreter enters an infinite evaluation; it will never be able to apply the procedure denoted by test, because it will never be able to compute the second argument.

In the case of normal-order evaluation, operands are not evaluated until their values are needed. (test 0 (p)) would be turned into 0 and then evaluated. And '0' evaluates to 0.

1.1.7 Example: Square Roots by Newton’s Method

  • Procedures are analogous to mathematical functions: ``[t]hey specify a value that is determined by one or more parameters''. (21-22)
  • However, procedures are different from mathematical functions in some respects. A mathematical function can tell us, say, whether a certain number is the square root of x or not. That, however, does not describe a procedure. It does not tell us how to find the square root of x.
  • More generally, mathematics is usually concerned with ``declarative knowledge'', whereas computer science is concerned with ``imperative knowledge''.
  • Iteration can be accomplished by calling a procedure. We don't need any looping construct.

Exercise 1.6

Exercise:

Alyssa P. Hacker doesn't see why if needs to be provided as a special form. ``Why can't I just define it as an ordinary procedure in terms of cond?'' she asks. Alyssa's friend Eva Lu Ator claims this can indeed be done, and she defines a new version of if:

(define (new-if predicate then-clause else-clause)
  (cond (predicate then-clause)
        (else else-clause)))

Eva demonstrates the program for Alyssa:

(new-if (= 2 3) 0 5)
5
(new-if (= 1 1) 0 5)
0

Delighted, Alyssa uses new-if to rewrite the square-root program:

(define (sqrt-iter guess x)
  (new-if (good-enough? guess x)
          guess
          (sqrt-iter (improve guess x)
                     x)))

What happens when Alyssa attempts to use this to compute square roots? Explain.

Answer:

cond is a special form. if, too, is a special form. new-if, instead, is not a special form. It is an ordinary combination.

Now, the evaluation of a combination entails the evaluation of both the operator and the operands. With Eva's new-if, then, sqrt-iter calls itself ad infinitum and a stack overflow occurs.

In fact, if we replace the new-if — a combination — with the cond — a special form — it would evaluate to, then things will work as originally intended.

Exercise 1.7

The good-enough? test used in computing square roots will not be very effective for finding the square roots of very small numbers. Also, in real computers, arithmetic operations are almost always performed with limited precision. This makes our test inadequate for very large numbers. Explain these statements, with examples showing how the test fails for small and large numbers. An alternative strategy for implementing good-enough? is to watch how guess changes from one iteration to the next and to stop when the change is a very small fraction of the guess. Design a square-root procedure that uses this kind of end test. Does this work better for small and large numbers?

Answer:

This is a pretty small number: \(0.00025\). It's square root is \(\sqrt{0.00025} = 0.0158113883\) (I have used a calculator).

Let's try to apply our test to the right answer divided by two.

(good-enough? (/ 0.0158113883 2) 0.00025)

The test returns true; that is, it's telling us that half of the right answer is good enough. I take that as a failure.

When we are dealing with very small numbers, then the \(0.001\) used in our test is too big for our purposes.

This is a pretty big number: \(7894561230.0123456789\). The square root of this number is \(\sqrt{7894561230.0123456789} = 88851.3434339\) (I have used a calculator).

Let's see whether are test consider the right answer as good enough…

(good-enough? 88851.3434339 7894561230.0)

This evaluate to nil… The problem seems to lie in the application of (the procedure named by) square, which gives a rather imprecise result.

This is the body of good-enough?:

(< (abs (- (square 88851.3434339) 7894561230.0)) 0.001)

It evaluates to nil, because the difference between the square of the guess and the radicant is greater than 0.001. However, the actual square of the radicant does not differ from the radicant of a value greater than 0.001.

Here is my version of an improved version of good-enough? following the authors' suggestion:

(defun good-enough-improved? (new-guess old-guess)
  (< (abs (- (abs old-guess) (abs new-guess))) (/ old-guess 10000000.0)))

(defun sqrt-iter2 (new-guess old-guess x)
  (if (good-enough-improved? new-guess old-guess)
      new-guess
    (sqrt-iter2 (improve new-guess x) new-guess x)))

(defun sqrt2 (x)
  (sqrt-iter2 1.0 x x))

My version seems to work much better for small numbers:

(sqrt 0.00025) ;; => 0.033869844451165365
               ;;    bad!
(sqrt2 0.00025) ;; => 0.015811388300841896
                ;;    As good as the built-in emacs lisp sqrt function!

But there doesn't seem to be difference with big numbers:

(sqrt 7894561230.0)  ;; 88851.34343385023
(sqrt2 7894561230.0) ;; 88851.34343385023

This is so, I think, because, even if good-enough? returns nil when it shouldn't, improve is called until we get something that differs from the radicant for less than 0.001… is this correct?

Exercise 1.8

Exercise:

Newton’s method for cube roots is based on the fact that if \(y\) is an approximation to the cube root of \(x\), then a better approximation is given by the value \(\frac{x/y^2 + 2y}{3}\). Use this formula to implement a cube-root procedure analogous to the square-root procedure. (In 1.3.4 we will see how to implement Newton’s method in general as an abstraction of these square-root and cube-root procedures.)

Answer:

(defun cuberoot (x)
  (cuberoot-iter2 1.0 x x))

(defun cuberoot-iter (new-guess old-guess x)
  (if (good-enough-improved? new-guess old-guess)
      new-guess
    (cuberoot-iter2 (improve-cr new-guess x) new-guess x)))

(defun improve-cr (guess x)
  (/ (+ (/ x (square guess))
        (* 2 y))
     3))


1.1.8 Procedures as Black-Box Abstractions

  • Procedural abstraction.
  • Bound variables.
  • Free variables.
  • Block structure.
  • Lexical scoping.

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

Created with Emacs 28.2 (Org mode 9.5.5)