SICP 05A - Assignment


1 Functional Programs

Why we would ever add such a bad thing (assignments), if we could do much without it? In terms of adding new features to our language, we should always follow this rule: we're only going to add a new feature to our language because there are good reasons to do so.

In this case, the ability you get from assignments is that with assignment statements you are able to break a problem into particular pieces in a way that you woulnd't be able to do without them. In other words, assignments give you another means of decomposition.

So far we have written functional programs, which sort-of encode mathematical truths. For example consider the following factorial procedure

(define (fact n)
  (cond ((= n 1) 1)
        (else (+ n (fact (- n 1))))))

which encodes the following mathematical definition:

\[\forall n \in \mathbb{N}: \;\;\; n! := \begin{cases} 1 \,\,&,\,\, n = 1 \\ n \cdot (n-1)! \,\,&,\,\, n \geq 1 \\ \end{cases}\]

Whenever we work with statements of this sort, we can understand exactly how the process evolves by applying the substitution model to the procedure definition.

(fact 4)
(* 4 (fact 3))
(* 4 (* 3 (fact 2)))
(* 4 (* 3 (* 2 (fact 1))))
(* 4 (* 3 (* 2 1)))
(* 4 (* 3 2))
(* 4 6)
24

Notice that in each step of the substitution process, an underlying truth is preserved. That is, we go from true statament to true statement, until we get our answer.

There might be different ways of expressing true statements. These different ways may evolve into different processes. This is the case for the sum procedure, which can be express both by a recursive process

(define (+ n m)
  (cond ((= n 0) m)
        (else (1+ (+ (-1+ n) m)))))

which encodes this mathematical rule

\[n + m = \begin{cases} m \;\;&, \;\; n = 0 \\ ((n - 1) + m) + 1 \;\;&,\;\; n > 0 \\ \end{cases}\]

as well as by an iterative process

(define (+ n m)
  (cond ((= n 0) m)
        (else (+ (-1+n) (1+ m)))))

which encodes this other mathematical rule

\[n + m = \begin{cases} m \;\;&, \;\; n = 0 \\ (n-1) + (m+1) \;\;&,\;\; n > 0 \\ \end{cases}\]

With this sort of scaffolding we have the flexibility to talk about both the function being computed as well as the method by which we compute it.

2 The Assignment Operator

The syntax used for the assignment operator is described as follows

(set! <var> <value>)   

Its worth notice that the syntax "set!" with the escalmation mark at the end is only useful to humans reading the code, and not the machine interpreting it. It means "this is an assignment operator, be careful!"

By introducing the assignment operation we inevitably introduce the notion of time. That is, now there is a time when something happens. That "something" is exactly that assignment operation. Said in another way, an assignment is the first thing we have that produces the difference between a before and an after

Thus we get

<before>
(set! <var> <value>)
<after>

such that after the operation <var> has the value <value> .


Think about the factorial procedure, and think about how it is essentially the same as the function, because anywhere I write it, anytime I write it, I always get the same answer. Independent of what context its in, it uniquely maps the arguments to the answer.

When we introduce assignments, this is no longer the case. Consider the following procedure

(define count 1)

(define (demo x)
  (set! count (1+ count))
  (+ x count))

Let us execute it: the first time we execute it we get an answer of \(5\), because initially count is \(1\). But then, if I execute it again, I get an answer of \(6\). Thus, the same expression leads to two different answers depending upon time. This means that this procedure is not a function anymore.

(demo 3) ;; returns 5
(demo 3) ;; returns 6

3 The Failure of the Substitution Model

This is also the first place where the substituion model doesn't work anymore, because the substitution model only allows to model static phenomenons, and with the introduction and usage of assignment we have a dynamic processes which evolves over time.

Consider the demo procedure, if we substitute the same value on both istances of the name count we get a wrong answer, because in reality they contain different values.

The substitution model describes static truths. Here instead we have truths that change. The new model we'll have to work with has to treat such variables differently. In particular, variables will no longer be referred to the values they have, but rather to some sort of place where the value is stored.

Let us write the functional version of factorial by an iterative process

(define (fact n)
  (define (fact-iter cur i)
    (if (eq? i n)
        cur
        (fact-iter (* cur i) (+ i 1))))
  (fact-iter 1 1))

Let us now write a similar sort of program but with assignments, which we will call the imperative version.

(define (fact n)
  (let ((i 1) (m 1))
    (define (loop)
      (cond ((> 1 n) m)
            (else
             (set! m (* i m))
             (set! i (+ i 1))
             (loop))))
    (loop)))

The substitution model says to "copy" the body of the procedure in which the formal parameters are substituted by the arguments. In this imperative version instead I'm not worried about copying, because I'm changing the values of both i and m .

Notice however that in the imperative version I can make more mistakes than in the functional version. For example, if I interchange the assignments between m and i , then the procedure would not compute the same values as before. This is because m depends on the previous value of i .

Thus, in order to introduce this new statament we need:

  1. A new model of computation that allows us to understand what is going on;

  2. A damn good reason for introducing such a statement.

Observation 1: Notice that define is used to set the value of something the first time. With only defines you cannot change the state of the program, because define happens only once for everything.

Observation 2: The let expression is simply an applied lambda expression, that is

(let ((var1 e1) (var2 e2))
     e3)

is equivalent to

((lambda (var1 var2) e3)
  e1
  e2)

4 The Environment Model

We will now create a new model of computation. This new model is much more complicated than the substitution model and its called the environment model.


4.1 Definitions

Before starting we have to introduce a bunch of terminology regarding names.



4.1.1 Bound Variables

We say that a variable \(V\) is bound in an expression \(E\) if the meaning of \(E\) is unchanged by the uniform replacement of a variable \(w\) not occurring in \(E\), for every occurence of \(v\) in \(E\).

Example 1: For example, in the following expression

\[\forall x \; \exists y \; P(x, y)\]

the meaning of \(x\) and \(y\) are bound, because the meaning of this expression does not depend on the particular letters I use to describe \(x\) and \(y\). If I were to change \(w\) for \(x\) it would be the same sentence.

Example 2: Another case would be the following

\[\int_0^1 \frac{dx}{1 + x^2}\]

Where in this case its \(x\) the bound variable.



4.1.2 Quantifier

A quantifier is a symbol that binds a variable. Instead of using \(\forall\), \(\exists\) and \(\int\) symbols, in programming we'll use the symbol \(\lambda\) (lambda).

Consider the following procedure

(lambda (y) ((lambda (x) (* x y)) 3))

this procedure has two bound variables: \(x\) and \(y\).

The concept of bound variable is crucial for modularity: if we have two people writing out programs, it shouldn't matter what names they use internal to their own programs. Thus, the previous procedure is equivalent to

(lambda (y) ((lambda (z) (* z y)) 3))

where \(x\) has been uniformly substituted by \(z\).



4.1.3 Free Variables

There are also some variables that are not bound, such as \(y\) in the following procedure

(lambda (x) (* x y))

We call these variables free variables. We say that a variable \(v\) is free in an expression \(E\) if the meaning of \(E\) is changed by the uniform replacement of a variable, \(w\), not occuring in \(E\), for every occurence of \(v\) in \(E\).

In the previous expression \(y\) is a free variable. Another example

(lambda (y) ((lambda (x) (* x y)) 3))

This procedure has a free variable in it, which is \(*\).



4.1.4 Scope

Let us now introduce the concept of a scope.

If \(x\) is a bound variable in \(E\), then there is a lambda expression where it is bound. We call the list of formal parameters of the lambda expression the bound variable list and we say that the lambda expression binds the variables declared in its bound variable list. In addition, those parts of the expression where a variable has a value defined by the lambda expression which binds it is called the scope of the variable.

The only way we get a bound variable is through a lambda expression. The only thing that makes names is lambda.


4.2 Environment

We are now ready to introduce our new model of computation. Notice that with the previously introduce concepts we can now talk about the value of a name as being stored in a particular place.

Lets now introduce the concept of an environment. An environment is a way of doing substitution virtually, it represents a place where something is stored. An environment is a function, or a structured table and its made up of things called frames. Frames are piece of an environemnts which are chained together with parent-links.

Consider the following image

In this image we have four enviroments and three frames:

  • Environemnt \(A\) has three bound variables: \(x\), \(z\) and \(y\). We say that the value of \(x\) in frame II of env \(A\) "shadows" the value of \(x\) in frame I of env \(A\).

  • Environment \(B\) has three bound variables: \(m\), \(y\) and \(x\). This time is the value \(y = 2\) which shadows the value \(y = 5\). Thus, when we ask for the value of \(y\) working in env \(B\), we get \(2\), and not \(5\).

The frames correspond to the application of procedures.


4.3 Procedure

Let us now understand a procedure. A procedure is made of two parts:

  • The first part refers to some code that can get executed.

  • The second part refers to an environemnt. This environment is used to capture the value of the free variable that occur in the procedure.


4.4 The Environment Model Rules

The new rules for evaluating using our new model of computation are the following ones:

  • Rule 1 (How do you apply a procedure to its arguments?): A procedure object is applied to a set of arguments by constructing a frame which binds the formal parameters of the procedure to the actual arguments of the call, and then evaluating the body of the procedure in the context of the new environment constructed. The new frame has as its enclosing enviroment the enviroment part of the procedure object being applied.

    Suppose I have some enviroment and some procedure

  • Rule 2 (How do you construct procedures?): A lambda-expression is evaluated relative to a given enviroment as follows: a new procedure object is formed, combining the text (code) of the lambda-expression with a pointer to the enviroment of evaluation.

The environment is a list of frames. It can also be thought of as the pointer to the first frame in the list, because once you have such pointer you have them all.

As a consequence of this new model, we have that the same lambda expression can be evaluated in two different environments producing two different procedures, even though such procedure share the same code, that is their definition is written with the same characters.


4.5 Example

Let us now understand better why we introduced all of this complex stuff. Let's start with a very simple program.

(define make-counter
  (lambda (n)
    (lambda ()
      (set! n (1+ n))
      n)))

In order to investigate the behavior of such procedure we have to create an environment. Suppose we start with a global enviroment the machine is born with. This global enviroment has a bunch of things like \(+\), \(*\), \(/\), \(-\), \(\text{car}\).

Let us now do some operation

(define c1 (make-counter 0))

Graphically we get,

Notice the steps that were taken:

  1. To evaluate the define statement we have to evaluate the expression (make-counter 0) in the global enviroment.

  2. I then look up make-counter , and see that it is a procedure. Thus, I use Rule 1 to apply a procedure to its arguments by constructing a new frame in which \(n = 0\). I then link up the frame to the global enviroment, since that was the enviroment where make-counter was defined.

  3. When I evaluate the body of make-counter I need to evaluate the lambda expression (lambda () ...) in the newly constructed enviroment. I thus need to make a procedure object, with the enviroment of the procedure was the frame where \(n = 0\).

  4. Finally, I link up the name \(c1\) with the procedure just created.

We can now make a new counter by executing

(define c2 (make-counter 10))   

Graphically now our system is represented as follows

Let us now execute these two counters

(c1)
1
(c2)
11
(c1)
2
(c2)
12

Notice that each counter is a seperate computational object. That is, each counter has its own version of \(n\), which gets incremented after each successive call.

5 Computational Objects

What is an object?

We like to think in terms of objects because it is economical. When I say "I'm an object", and "you are an object", we are separating the world into two pieces. The world thus becomes easy to reason about. Yes, its true that two objects are never really independent from eachothers in an absolute sense, but still, it is also true that most of the variables that define my state are decoupled from the variables that define your state, and therefore we can think of them as being independent of eachothers. With this approach instead of having the cartesian product of the two state variables sets, we can simply "sum" them.

The bugs that we see in quantum mechanims are mainly due to the fact that we are made to think that things are broken up in independent pieces, while there is actually more coupling than we see in the surfaces.


5.1 When are two Objects Different?

How would we know if we had objects at all? Couldn't it be a sort-of optical illusion? How can we tell we have two different objects. Well, an idea would be to change one and see if the other changes aswell.

How would we know if something changed? We would have to look at before and after the change.

"What is the identity of me? I don't know", cit.

By introducing assignment and objects we have opened ourselves up to all the horrible question of philosophy. That's why mathematics is a lot cleaner.


5.2 Actions and Identity

We say that an action \(A\) had an effect on an object \(I\) (or equivalently, that \(X\) was changed by \(A\)) if some property \(P\) which was true of \(X\) before \(A\) became false of \(X\) after \(A\).

We say that two objects, \(X\) and \(Y\), are the same if any action which has an effect on \(I\) has the same effect on \(Y\).


5.3 Object-Oriented Programming

Objects are very useful for intellectual economy. In particular they are incredibly useful because we like to think that the world is made up of independent objects made up of independent local states.

When we want to build complex systems we want there to be some isomorphisms between the objects in the world and the objects in our mental model. This is why we invent notions such as object-oriented programming.

6 Cesaro's Method for Estimating \(\pi\)

If I take two integers at random \(n_1\), \(n_2\), and consider their gcd, it can either by \(1\) or not \(1\). The probability that two random numbers have gcd equal to \(1\) is related to \(\pi\) in the following way

\[P\Big(\text{gcd}(n_1, n_2) = 1\Big) = \frac{6}{\pi^2}\]

we can thus approximate \(\pi\) by estimating such probability. And to estimate such probability we can do lots of experiments and compute the ratio between those that succeed and those that fail.


6.1 With Assignments

By using assignments we can implement a pseudo-random number generator as follows

(define rand
  (let ((x random-init))
    (lambda ()
      (set! x (rand-update x))
      x)))

We can then use this procedure in the cesaro procedure, which computes the gcd of two random numbers and checks if it is equal to \(1\).

(define (cesaro)
  (= (gcd (rand) (rand)) 1))

While for estimating the probability we can use the monte-carlo technique, which computes the ratio between those that succeed and those that fail.

(define (monte-carlo trials experiment)
  (define (iter remaining passed)
    (cond ((= remaining 0)
           (/ passed trials))
          ((experiment)
           (iter (- remaining 1)
                 (+ passed 1)))
          (else
           (iter (- remaining 1)
                 (passed)))))
  (iter trials 0))

Putting all of this together we get

(define (estimate-pi n)
  (sqrt (/ 6 (monte-carlo n cesaro))))


6.2 Without Assignments

Suppose I would like the previous procedure, but this time without assignment. Then I would get the following code

(define (estimate-pi n)
  (sqrt (/ 6 (random-gcd-test n))))

(define (random-gcd-test trials)
  (define (iter remaining passed x)
    (let ((x1 (rand-update x)))
      (let ((x2 (rand-update x1)))
        (cond ((= remaining 0)
               (/ passed trials)
               ((= (gcd x1 x2) 1)
                (iter (-1+ remaining)
                      (1+ passed)
                      x2))
               (else
                (iter (-1+ remaining)
                      passed
                      x2)))))))
  (iter trials 0 random-seed))

Notice that in this version the state of the random number generator has leaked out into my procedure that does the monte-carlo experiment. The cesaor procedure also has leaked out. This means that my monte-carlo method is no longer general.

Thus, I lost the ability to decompose a problem into pieces because I wasn't willing to accept the feedback process that happens inside a random number generator that was possible by having an assignment to a state variable that was confined to the generator.

"Things are seldom what they seem, Skim milk masquerades as cream..." Gilbert and Sullivan (H.M.S. Pinafore).