首页 > 代码库 > [SICP Notes] 1.1 The Elements of Programming

[SICP Notes] 1.1 The Elements of Programming

About the Book

To know more about programming, I have decided to start reading the famous Structure and Interpretation of Computer Programs (SICP, or the Wizard Book), which is first published by MIT in 1985. The Wizard Book used MIT Scheme, which is a dialect of Lisp, as the demonstrating language. This language is so different from the languages I know (C++ and JavaScript, which are both inspired by C). However, the book does not talk much about the characteristics of the language. It focus on teaching important concepts in Computer Science, but not teaching a particular language. I am sure that the book and the interesting language can help me to become a better programmer! (but not just a coder XD) This book is available online under Creative Common (CC) so you may take a look at it too!


Important Quotes from the Book

1.1.3  Evaluating Combinations


To evaluate a combination, do the following:
  1. Evaluate the subexpressions of the combination. (recursively!)
  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).
We take care of the primitive cases by stipulating that
  • the values of numerals are the numbers that they name,
  • the values of built-in operators are the machine instruction sequences that carry out the corresponding operations, and
  • the values of other names are the objects associated with those names in the environment.


1.1.5  Applicative Order Versus Normal Order


The evaluation model used in Lisp first evaluates the operator and operands and then applies the resulting procedure to the resulting arguments. This ``evaluate the arguments and then apply‘‘ method is calledapplicative-order evaluation.

An alternative evaluation model would not evaluate the operands until their values were needed. Instead it would first substitute operand expressions for parameters until it obtained an expression involving only primitive operators, and would then perform the evaluation. This alternative ``fully expand and then reduce‘‘ evaluation method is known asnormal-order evaluation.


1.1.7  Mathematical Functions Versus Computer Procedures


The contrast between function and procedure is a reflection of the general distinction between describing properties of things and describing how to do things, or, as it is sometimes referred to, the distinction betweendeclarative knowledge and imperative knowledge. Inmathematics we are usually concerned with declarative (what is) descriptions, whereas in computer science we are usually concerned with imperative (how to) descriptions.


1.1.8  Procedures as Black-Box Abstractions


For example, when we define the good-enough? procedure in terms of square, we are able to regard the square procedure as a ``black box.‘‘ We are not at that moment concerned with how the procedure computes its result, only with the fact that it computes the square. The details of how the square is computed can be suppressed, to be considered at a later time. Indeed, as far as the good-enough? procedure is concerned, square is not quite a procedure but rather an abstraction of a procedure, a so-calledprocedural abstraction. At this level of abstraction, any procedure that computes the square is equally good.

Local names

A formal parameter of a procedure has a very special role in the procedure definition, in that it doesn‘t matter what name the formal parameter has. Such a name is called abound variable, and we say that the procedure definitionbinds its formal parameters. The meaning of a procedure definition is unchanged if a bound variable is consistently renamed throughout the definition. If a variable is not bound, we say that it is free. The set of expressions for which a binding defines a name is called thescope of that name. In a procedure definition, the bound variables declared as theformal parameters of the procedure have the body of the procedure as their scope.

Internal definitions and block structure

Lexical scoping dictates that free variables in a procedure are taken to refer to bindings made by enclosing procedure definitions; that is, they are looked up inthe environment in which the procedure wasdefined.

In lexical scoping (or lexical scope; also called static scoping orstatic scope), if a variable name‘s scope is a certain function, then its scope is the program text of the function definition: within that text, the variable name exists, and is bound to the variable‘s value, but outside that text, the variable name does not exist. By contrast, in dynamic scoping (or dynamic scope), if a variable name‘s scope is a certain function, then its scope is the time-period during which the function is executing: while the function is running, the variable name exists, and is bound to its variable, but after the function returns, the variable name does not exist. This means that if functionf invokes a separately defined function g, then under lexical scoping, functiong does not have access to f‘s local variables (since the text ofg is not inside the text of f), while under dynamic scoping, functiong does have access to f‘s local variables (since the invocation ofg is inside the invocation of f).
(Source: Wikipedia)


My Solution to the Exercises


Exercise 1.1

In this exercise, I only need to write down the result of each expression. I have put a semi-colon(;) before the result.

10
;10
(+ 5 3 4)
;12
(- 9 1)
;8
(/ 6 2)
;3
(+ (* 2 4) (- 4 6))
;6
(define a 3)
(define b (+ a 1))
(+ a b (* a b))
;19
(= a b)
;#f
(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

In this exercise, I need to translate a mathematical expression into a complex combination in Scheme.

(/ (+ 5
      4
      (- 2
	 (- 3
	    (+ 6
	       (/ 4 3)))))
   (* 3
      (- 6 2)
      (- 2 7)))
(well, I have abused the use of indentation in this exercise...)

Exercise 1.3

In this exercise, I need to write a procedure that take three numbers as arguments and returns the sum of the squares of the two larger numbers.

(define (sum-of-squares-of-larger-two a b c)
  (cond ((and (<= a b) (<= a c)) (+ (* b b) (* c c)))
		((and (<= b a) (<= b c)) (+ (* a a) (* c c)))
		(else (+ (* a a) (* b b)))))

Exercise 1.4

In this exercise, I need to explain the behaviour of the following procedure:

(define (a-plus-abs-b a b)
  ((if (> b 0) + -) a b))
This procedure returns the sum of a and the absolute value of b.

When b is positive, the procedure return a+b.
When b is 0 or negative, the procedure returns a-b (a-b = a-(-|b|) = a+|b|, given b<=0)

Exercise 1.5

Given the following code, is the result different when applicative order and normal order are used?

(define (p) (p))

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

(test 0 (p))

Applicative Order:

(test 0 (p))

This method would first evaluate all the subexpressions of the combination.
(p) calls (p) again so there is an infinite loop. (runtime_error)

Normal Order:

(test 0 (p))
(if (= 0 0) 0 (p))
0

This method would first substitute operand expressions for parameters until it obtained an expression involving only primitive operators, and would then perform the evaluation. (p) is not evaluated until it is needed, so no infinite loop appears. (0 is returned)


Exercise 1.6

Why the built-in if is a special form instead of a procedure? Given the following definition of a new-if:

(define (new-if predicate then-clause else-clause)
  (cond (predicate then-clause)
        (else else-clause)))
This new-if looks pretty good when applying to simple cases. However, it is not that good when recursion is used in the clauses. Consider this:

(define (sqrt-iter guess x)
  (new-if (good-enough? guess x)
          guess
          (sqrt-iter (improve guess x)
                     x)))
Since the interpreter evaluate combinations in applicative order, it would evaluate all the subexpressions in (new-if predicate then-clause else-clause). As the else-clause of new-if in sqrt-iter is sqrt-iter itself, using new-if would invoke an infinite loop.

If the special form if is used instead, the interpreter would not evaluate both the then-clause and else-clause. The self-calling then clause is only evaluated when predicate returns false.

Exercise 1.7

In the sample (sqrt x) procedure in this chapter, a good-enough procedure is used to judge whether the approximation of the sqrt is accurate enough:

(define (good-enough? guess x)
  (< (abs (- (square guess) x)) 0.001))
We say that the guess is good enough if the difference between guess square and x is fewer than a value, 0.001 here. However, this method has its limitation. When x is very big or very small, the fixed value of 0.001 cannot play its role effectively. (you can try!) So I write a new procedure for this exercise:

(define (average x y)
  (/ (+ x y) 2.0))

(define (square x) (* x x))

(define (sqrt x)
  (define (good-enough? guess old-guess)
    (< (abs (/ (- guess old-guess)
            old-guess))
       0.00000001))
  (define (improve guess)
    (average guess (/ x guess)))
  (define (sqrt-iter guess old-guess)
    (if (good-enough? guess old-guess)
        guess
        (sqrt-iter (improve guess)
                   guess)))
  (sqrt-iter 1.0 -1.0))
Notice that I have put procedures like good-enough? and improve into the definition of sqrt. Doing so can reduce confusion (as good-enough? and guess are common names but should only be useful in the sqrt procedure) and reduce the number of parameters in each procedure (x needs not to be passed due to its lexical scope).


Exercise 1.8

This one is similar to 1.7 but this time I am solving for the cube root.

(define (pow x y)
  (if (= y 0)
      1
      (* x (pow x (- y 1)))))

(define (cube-root x)
  (define (good-enough? guess old-guess)
    (< (abs (/ (- guess old-guess)
               old-guess))
       0.00000001))
  (define (improve guess)
    (/ (+ (/ x (pow guess 2))
          (* 2 guess))
       3))
  (define (cube-root-iter guess old-guess)
    (if (good-enough? guess old-guess)
        guess
        (cube-root-iter (improve guess)
                        guess)))
  (cube-root-iter 1.0 -1.0))

(cube-root -8)
I have added a test case -8 to show that this procedure can handle the negative root as well.


Do you find coding in Scheme interesting? Or are you interested in the Wizard Book? Join me to read the book so that we can share our thoughts!