Home » Courses » Electrical Engineering and Computer Science » Structure and Interpretation of Computer Programs » Video Lectures » 5A: Assignment, State, and Side-effects
Flash and JavaScript are required for this feature.
Download the video from Internet Archive.
Topics covered: Assignment, State, and Side-effects
Instructors: Hal Abelson and Gerald Jay Sussman
Subtitles for this course are provided through the generous assistance of Henry Baker, Hoofar Pourzand, Heather Wood, Aleksejs Truhans, Steven Edwards, George Menhorn, and Mahendra Kumar.
5A: Assignment, State, and ...
[MUSIC PLAYING]
PROFESSOR: Well, so far we've invented enough programming to do some very complicated things. And you surely learned a lot about programming at this point. You've learned almost all the most important tricks that usually don't get taught to people until they have had a lot of experience. For example, data directed programming is a major trick, and yesterday you also saw an interpreted language.
We did this all in a computer language, at this point, where there was no assignment statement. And presumably, for those of you who've seen your Basic or Pascal or whatever, that's usually considered the most important thing. Well today, we're going to do some thing horrible. We're going to add an assignment statement.
And since we can do all these wonderful things without it, why should we add it? An important thing to understand is that today we're going to, first of all, have a rule, which is going to always be obeyed, which is the only reason we ever add a feature to our language is because there is a good reason. And the good reason is going to boil down to the ability, you now get an ability to break a problem into pieces that are different sets of pieces then you could have broken it down without that, give you another means of decomposition.
However, let's just start. Let me quick begin by reviewing the kind of language that we have now. We've been writing what's called functional programs. And functional programs are a kind of encoding of mathematical truths. For example, when we look at the factorial procedure that you see on the slide here, it's basically two clauses. If n is one, the result is one, otherwise n times factorial n minus one. That's factorial of n. Well, that is factorial of n.
And written down in some other obscure notation that you might have learned in calculus classes, mathematical logic, what you see there is if n equals one, for the result of n factorial is one, otherwise, greater than one, n factorial is n times n minus one factorial.
True statements, that's the kind of language we've been using. And whenever we have true statements of that sort, there is a kind of, a way of understanding how they work which is that such processes can be involved by substitution.
And so we see on the second slide here, that the way we understand the execution implied by those statements in arranged in that order, is that you do successive substitutions of arguments for formal parameters in the body of a procedure.
This is basically a sequence of equalities. Factorial four is four times factorial three. That is four times three times factorial of two and so on. We're always preserving truth. Even though we're talking about true statements, there might be more than one organization of these true statements to describe the computation of a particular function, the computation of the value of a particular function.
So, for example, looking at the next one here. Here is a way of looking at the sum of n and m. And we did this one by a recursive process. It's the increment of the sum of the decrement of n and m. And, of course, there is some piece of mathematical logic here that describes that. It's the increment of the sum of the decrement of n and m, just like that. So there's nothing particularly magic about that.
And, of course, if we can also look at an iterative process for the same, a program that evolves an iterative process, for the same function. These are two things that compute the same answer. And we have equivalent mathematical truths that are arranged there. And just the way you arrange those truths determine the particular process. In the way choose and arrange them determines the process that's evolved.
So we have the flexibility of talking about both the function to be computed, and the method by which it's computed. So it's not clear we need more. However, today I'm going to this awful thing. I'm going to introduce this assignment operation.
Now, what is this? Well, first of all, there is going to be another kind of kind of statement, if you will, in a programming language called Set! Things that do things like assignment, I'm going to put exclamation points after. We'll talk about what that means in a second. The exclamation point, again like question mark, is an arbitrary thing we attach to the symbol which is the name, has no significance to the system. The only significance is to me and you to alert you that this is an assignment of some sort.
But we're going to set a variable to a value. And what that's going to mean is that there is a time at which something happens. Here's a time. If I have time going this way, it's a time access. Time progresses by walking down the page. Then an assignment is the first thing we have that produces the difference between a before and an after.
All the other programs that we've written, that have no assignments in them, the order in which they were evaluated didn't matter. But assignment is special, it produces a moment in time. So there is a moment before the set occurs and after, such that after this moment in time, the variable has the value, value. Independent of what value it had before, set! changes the value of the variable.
Until this moment, we had nothing that changed. So, for example, one of the things we can think of is that the procedures we write for something like factorial are in fact pretty much identical to the function factorial. Factorial of four, if I write fact4, independent of what context it's in, and independent of how many times I write it, I always get the same answer. It's always 24. It's a unique map from the argument to the answer.
And all the programs we've written so far are like that. However, once I have assignment, that isn't true. So, for example, if I were to define count to be one. And then I'm going to define also a procedure, a simple procedure called demo, which takes argument x and does the following operations. It first sets x to x plus one. My gosh, this looks just like FORTRAN, right-- in a funny syntax. And then add to x count, Oh, I just made a mistake.
I want to say, set! count to one plus count. It's this thing defined here. And then plus x count. Then I can try this procedure. Let's run it. So, suppose I get a prompt and I say, demo three.
Well, what happens here? The first thing that happens is count is currently one. Currently, there is a time. We're talking about time. x gets three. At this moment, I say, oh yes, count is incremented, so count is two. two plus three is five. So the answer I get out is five. Then I say, demo of say, three again.
What do I get? Well, now count is two, it's not one anymore, because I have incremented it. But now I go through this process, three goes into x, count becomes one plus count, so that's three now. The sum of those two is six, so the answer is six.
And what we see is the same expression leads to two different answers, depending upon time. So demo is not a function, does not compute a mathematical function. In fact, you could also see why now, of course, this is the first place where the substitution model isn't going to work. This kills the substitution model dead.
You know, with quotation there were some little problems that a philosopher might notice with the substitutions, because you have to worry about what deductions you can make when you substitute into quotes, if you're allowed to do that at all. But here the substitution model is dead, can't do anything at all.
Because, supposing I wanted to use a substitution model to consider substituting for count? Well, my gosh, if I substitute for here and here, they're different ones. It's not the same count any more. I get the wrong answer. The substitution model is a static phenomenon that describes things that are true and not things that change. Here, we have truths that change.
OK, Well, before I give you any understanding of this, this is very bad. Now, we've lost our model of computation. Pretty soon, I'm going to have to build you a new model of computation. But ours plays with this, just now, in an informal sense. Of course, what you already see is that when I have something like assignment, the model that we're going to need is different from the model that we had before in that the variables, those symbols like count, or x are no longer going to refer to the values they have, but rather to some sort of place where the value restored.
We're going to have to think that way for a while. And it's going to be a very bad thing and cause a lot of trouble. And so, as I said, the very fact that we're inventing this bad thing, means that there had better be a good reason for it, otherwise, just a waste of time and a lot of effort.
Let's just look at some of it just to play. Supposing we write down the functional version, functional meaning in the old style, of factorial by an iterative process. Factorial of n, we're going to iterate of m and i, which says if i is greater than n, then the result is m, otherwise, the result of iterating the product of i and m. So m is going to be the product that I'm accumulating. m is the product. And the count I'm going to increase by one. Plus, ITER, ELSE, COND, define.
I'm going to start this up. And these days, you should have no trouble reading something like this. What I have here is a product there being accumulated and a counter. I start them up both at one. I'm going to buzz the counter up, i goes to i plus one every time around. But that's only our putting a time on the process, each of this is just a set of truths, true rules. And m is going to get a new values of i and m, i times m each time around, and eventually i is going to be bigger than n, in which case, the answer's going to be m.
Now, I'm speaking to you, use time in this. That's just because I know how the computer works. But I didn't have to. This could be a purely mathematical description at this point, because substitution will work for this.
But let's set right down a similar sort of program, using the same algorithm, but with assignments. So this is called the functional version. I want to write down an imperative version. Factorial of n. I'm going to create my two variables. Let i initialize itself to one, and m be initialized to one, similar.
We'll create a loop which has COND greater than i, and if i is greater than n, we're done. And the result is m, the product I'm accumulating. Otherwise, I'm going to write down three things to do. I'm going to set! m to the product of i and m, set! i to the sum of i and one, and go around the loop again. Looks very familiar to you FORTRAN programmers. ELSE, COND, define, funny syntax though. Start the loop up, and that's the program.
Now, this program, how do we think about it? Well, let's just say what we're seeing here. There are two local variables, i and m, that have been initialized to one. Every time around the loop, I test to see if i is greater than n, which is the input argument, and if so, the result is the product being accumulated in m. However, if it's not the end of the loop, if I'm not done, then what I'm going to do is change the product to be the result of multiplying i times the current product.
Which is sort of what we were doing here. Except here I wasn't changing. I was making another copy, because the substitution model says, you copy the body of the procedure with the arguments substituted for the formal parameters. Here I'm not worried about copying, here I've changed the value of m. I also then change the value of i to i plus one, and go buzzing around.
Seems like essentially the same program, but there are some ways of making errors here that didn't exist until today. For example, if I were to do the horrible thing of not being careful in writing my program and interchange those two assignments, the program wouldn't compute the same function.
I get a timing error because there's a dependency that m depends upon having the last value of i. If I try to i first, then I've got the wrong value of i when I multiply by m. It's a bug that wasn't available until this moment, until we introduced something that had time in it.
So, as I said, first we need a new model of computation, and second, we have to be damn good reason for doing this kind of ugly thing. Are there any questions? Speak loudly, David.
AUDIENCE: I'm confused about, we've introduced set now, but we had let before and define before. I'm confused about the difference between the three. Wouldn't define work in the same situation as set if you introduced it a bit?
PROFESSOR: No, define is intended for setting something once the first time, for making it. You've never seen me write on a blackboard two defines in a row whose intention was to change the old value of some variable to a new one.
AUDIENCE: Is that by convention or--
PROFESSOR: No, it's intention. The answer is that, for example, internal to a procedure, two defines in a row are illegal, two defines in a row of the same variable. x can't be defined twice. Whether or not a system catches that error is a different question, but I legislate to you that define happens once on anything.
Now, indeed, in interactive debugging, we intend that you interacting with your computer will redefine things, and so there's a special exception made for interactive debugging. But define is intended to mean to set up something which will be forever that value after that point. It's as if all the defines were done at the beginning. In fact, the only legal place to put a define in Scheme, internal to a procedure, is just at the beginning of a lambda expression, the beginning of the body of a procedure.
Now, let of course does nothing like either of that. I mean, if you look at what's happening with a let, this happens again exactly once. It sets up a context where i and m are values one and one. That context exists throughout this scope, this region of the program. However, you don't think of that let as setting i again. It doesn't change it. i never changes because of the let. i gets created because of let.
In fact, the let is a very simple idea. Let does nothing more, Let a variable one to have value one; I'll write this down a little bit more neatly; Let's write, var one have value, the value of expression e1, and variable two, have this value of the expression e2, in an expression e3, is the same thing as a procedure of var one and var two, the formal parameters, and e3 being the body, where var one is bound to the value of e1, and var two gets the value of e2.
So this is, in fact, a perfectly understandable thing from a substitution point of view. This is really the same expression written in two different ways. In fact, the way the actual system works is this gets translated into this before anything happens.
AUDIENCE: OK, I'm still unclear as then what makes the difference between a let and a define. They could--
PROFESSOR: A define is a syntactic sugar, whereby, essentially a bunch of variables get created by lets and then set up once. OK, time for the first break, I think. Thank you. [MUSIC PLAYING]
Well let's see. I now have to rebuild the model of computation, so you understand how some such mechanical mechanism could work that can do what we've just talked about. I just recently destroyed your substitution model. Unfortunately, this model is significantly more complicated than the substitution model.
It's called the environment model. And I'm going to have to introduce some terminology, which is very good terminology for you to know anyway. It's about names. And we're going to give names to the kinds of names things have and the way those names are used. So this is a meta-description, if you will. Anyway, there is a pile of an unfortunate terminology here, but we're going to need this to understand what's called the environment model. We're about to do a little bit of boring, dog-work here.
Let's look at the first transparency. And we see a description of a word called bound. And we're going to 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 occurrence of v in e. Now that's a long sentence, so, I think, I'm going to have to say a little bit about that before we even fool around at all here.
Bound variables we're talking about here. And you've seen lots of them. You may not know that you've seen lots of them. Well, I suppose in your logic you saw a logical variables like, for every x there exists a y such that p is true of x and y from your calculus class. This variable, x, and this variable, y, are bound, because the meaning of this expression does not depend upon the particular letters I used to describe x and y. If I were to change the w for x, then said for every w there exists a y such that p is true of w and y, it would be the same sentence. That's what it means.
Or another case of this that you've seen is integral say, from 0 to one of dx over one plus x square. Well that's something you see all the time. And this x is a bound variable. If I change that to a t, the expression is still the same thing. This is a 1/4 of the arctan of one or something like that. Yes, that's the arctan of one. So bound variables are actually fairly common, for those of you who have played a bit with mathematics.
Well, let's go into the programming world. Instead of the quantifier being something like, for every, or there exists, or integral, a quantifier is a symbol that binds a variable. And we are going to use the quantifier lambda as being the essential thing that binds variables.
And so we have some nice examples here like that procedure of one argument y which does the following thing. It calls the procedure of one argument x, which multiplies x by y, and applies that to three. That procedure has the property there of two bound variables in it, x and y. This quantifier, lambda here, binds this y, and this quantifier, lambda, binds that x. Because, if I were to take an arbitrary symbol does not occur in this expression like w and replace all y's with w's in this expression, the expression is still the same, the same procedure.
And this is an important idea. The reason why we had such things like that is a kind of modularity. If two people are writing programs, and they work together, it shouldn't matter what names they use internal to their own little machines that they're building. And so, what I'm really telling you there, is that, for example, this is equivalent to that procedure of one argument y which uses that procedure of one argument d which multiplies z by y. Because nobody cares what I used in here. It's a nice example.
On the other hand, I have some variables that are not bound. For example, that procedure of one argument x which multiplies x by y. In this case, y is not bound. Supposing y had the value three, and z had the value four, then this procedure would be the thing that multiplies its argument by three. If I were to replace every instance of y with z, I would have a different procedure which multiplies every argument that's given by four.
And, in fact, we have a name for such a variable. Here, we say that a variable, v, is free in the expression, e, if the meaning of the expression, e, is changed by the uniform replacement of a variable, w, not occurring in e for every occurrence of v and e.
So that's why this variable over here, y, is a free variable. And so free variables in this expression-- And other examples of that is that procedure of one argument y, which is just what we had before, which uses that procedure of one argument x that multiplies x by y-- use that on three. This procedure has a free variable in it which is asterisk. See, because, if that has a normal meaning of multiplication, then if I were to replace uniformly all asterisks with pluses, then the meaning of this expression would change. That's what you mean by a free variable.
So, so far you've learned some logician words which describe the way names are used. Now, we have to do a little bit more playing around here, a little bit more. I want to tell you about the regions are over which variables are defined.
You see, we've been very informal about this up till now, and, of course, many of you have probably understood very clearly or most of you, that the x that's being declared here is defined only in here. This x is the defined only in here, and this y is defined only in here. We have a name for such an idea. It's called a scope.
And let me give you another piece of terminology. It's a long story. If x is a bound variable in e, then there is a lambda expression where it is bound. So the only way you can get a bound variable ultimately is by lambda expression. Then you may worry, does define quite an exception to this? And it turns out, we could always arrange things so you don't need any defines. And we'll see that in a while. It's a very magical thing. So define really can go away. The really, only thing that makes names is lambda . That's its job. And what's so amazing about a lot of things is you can compute with only lambda.
But, in any case, a lambda expression has a place where it declares a variable. We call it the formal parameter list or the bound variable list. We say that the lambda expression binds-- so it's a verb-- binds the variables declared in it's found variable list. In addition, those parts of the expression where the variable is defined, which was declared by some declaration, is called the scope of that variable. So these are scopes. This is the scope of y. And this is the scope of x-- that sort of thing.
OK, well, now we have enough terminology to begin to understand how to make a new model for computation, because the key thing going on here is that we destroyed the substitution model, and we now have to have a model that represents the names as referring to places. Because if we are going to change something, then we have a place where it's stored.
You see, if a name only refers to a value, and if I tried to change the name's meaning, well, that's not clear. There's nothing that is the place that that name referred to. How am I really saying it? There is nothing shared among all of the instances of that name. And what we really mean, by a name, is that we fan something out. We've given something a name, and you have it, and you have it, because I'm given you a reference to it, and I've given you a reference to it. And we'll see a lot about that.
So let me tell you about environments. I need the overhead projection machine, thank you. And so here is a bunch of environment structures. An environment is a way of doing substitutions virtually. It represents a place where something is stored which is the substitutions that you haven't done. It's a place where everything accumulates, where the names of the variables are associated with the values they have such that when you say, what dose this name mean, you look it up in an environment.
So an environment is a function, or a table, or something like that. But it's a structured sort of table. It's made out of things called frames. Frames are pieces of environment, and they are chained together, in some nice ways, by what's called parent links or something like that.
So here, we have an environment structure consisting of three environments, basically, a, b, and c. d is also an environment, but it's the same one, they share. And that's the essence of assignment. If I change a variable, a value of a valuable that lives here, like that one, it should be visible from all places that you're looking at it from. Take this one, x. If I change the x to four, it's visible from other places. But I'm not going to worry about that right now. We're going to talk a lot about that in a little while.
What do we have here? Well, these are called frames. Here is a frame, here's a frame, and here's a frame. a is an environment which consists of the table which is frame two, followed by the table labeled frame one. And, in this environment, in say this environment, frame two, x and y are bound. They have values. Sorry, in frame one-- In frame two, z is bound, and x is bound, and y is bound, but the value of x that we see, looking from this point of view, is this x. It's x is seven, rather than this one which is three. We say that this x shadows this x. From environment three-- from frame three, from environment b, which refers to frame three, we have variables n and y bound and also x. This y shadow this one. So the value, looking from this point of view, of y is two. The value for looking from this point of view and m is one. And the value, looking from this point of view, of x is three.
So there we have a very simple environment structure made out of frames. These correspond to the applications of procedures. And we'll see that in a second. So now I have to make you some other nice little structure that we build.
Next slide, we see an object, which I'm going to draw procedures. This is a procedure. A procedure is made out of two parts. It's sort of like a cons. However, it's the two parts. The first part refers to some code, something that can be executed, a set of instructions, if you will. You can think of it that way. And the second part is the environment. The procedure is the whole thing. And we're going to have to use this to capture the values of the free variables that occur in the procedure. If a variable occurs in the procedure it's either bound in that procedure or free. If it's bound, then the value will somehow be easy to find. It will be in some easy environment to get at. If it's free, we're going to have to have something that goes with the procedure that says where we'll go look for its value. And the reasons why are not obvious yet, but will be soon.
So here's a procedure object. It's a composite object consisting of a piece of code and a environment structure. Now I will tell you the new rules, the complete new rules, for evaluation. The first rule is-- there's only two of them. These correspond to the substitution model rules. And the first one has to do with how do you apply a procedure to its arguments? And a procedural object is applied to a set of arguments by constructing a new frame. That frame will contain the mapping of the former parameters to the actual parameters of the arguments that were supplied in the call.
As you know, when we make up a call to a procedure like lambda x times x y, and we call that with the argument three, then we're going to need some mapping of x to three. It's the same thing as later substituting, if you will, the three for the x in the old model. So I'm going to build a frame which contains x equals three as the information in that frame.
Now, the body of the procedure will then have to be evaluated which is this. I will be evaluated in an environment which is constructed by adjoining the new frame that we just made to the environment which was part of the procedure that we applied.
So I'm going to make a little example of that here. Supposing I have some environment. Here's a frame which represents it. And some procedure-- which I'm going to draw with circles here because it's easier than little triangles-- Sorry, those are rhombuses, rhomboidal little pieces of fruit jelly or something.
So here's a procedure which takes this environment. And the procedure has a piece of code, which is a lambda expression, which binds x and y and then executes an expression, e. And this is the procedure. We'll call it p. I wish to apply that procedure to three and four. So I want to do p of three and four.
What I'm going to do, of course, is make a new frame. I build a frame which contains x equals three, and y equals four. I'm going to connect that frame to this frame over here. And then this environment, with I will call b, is the environment in which I will evaluate the body of e. Now, e may contain references to x and y and other things. x and y will have values right here. Other things will have their values here.
How do we get this frame? That we do by the construction of procedures which is the other rule. And I think that's the next slide. Rule two, when a lambda expression is evaluated, relative to a particular environment-- See, the way I get a procedure is by evaluating the lambda expression.
Here's a lambda expression. By evaluating it, I get a procedure which I can apply to three. Now this lambda expression is evaluated in an environment where y is defined. And I want the body of this which contains a free version of y. y is free in here, it's bound over the whole thing, but it's free over here. I want that y to be this one. I evaluate this body of this procedure in the environment where y was created. That's this kind of thing, because that was done by application.
Now, if I ever want to look up the value of y, I have to know where it is. Therefore, this procedural was created, the creation of the procedure which is the result of evaluating that lambda expression had better capture a pointer or remember the frame in which y was bound. So that's what this rule is telling us.
So, for example, if I happen to be evaluating a lambda expression, lambda expression in e, lambda of say, x and y, let's call it g in e, evaluating that. Well, all that means is I now construct a procedure object. e is some environment. e is something which has a pointer to it. I construct a procedure object that points up to that environment, where the code of that is a lambda expression or whatever that translates into. And this is the procedure.
So this produces for me-- this object here, this environment pointer, captures the place where this lambda expression was evaluated, where the definition was used, where the definition was used to make a procedure, to make the procedure. So it picks up the environment from the place where that procedure was defined, stores it in the procedure itself, and then when the procedure is used, the environment where it was defined is extended with the new frame.
So this gives us a locus for putting where a variable has a value. And, for example, if there are lots of guys pointing in at that environment, then they share that place. And we'll see more of that shortly.
Well, now you have a new model for understanding the execution of programs. I suppose I'll take questions now, and then we'll go on and use that for something.
AUDIENCE: Is it right to say then, the environment is that linked chain of frames--
PROFESSOR: That's right.
AUDIENCE: starting with-- working all the way back?
PROFESSOR: Yes, the environment is a sequence of frames linked together. And the way I like to think about it, it's the pointer to the first one, because once you've got that you've got them all. Anybody else?
AUDIENCE: Is it possible to evaluate a procedure or to define a procedure in two different environments such that it will behave differently, and have pointers to both--
PROFESSOR: Oh, yes. The same procedure is not going to have two different environments. The same code, the same lambda expression can be evaluated in two environments producing two different procedures. Each procedure--
AUDIENCE: Their definition has the same name. Their operation--
PROFESSOR: The definition is written the same, with the same characters. I can evaluate that set of characters, whatever, that list structure that defines, that is the textual representation. I can evaluate that in two different environments producing two different procedures. Each of those procedures has its own local sets of variables, and we'll see that right now. Anybody else? OK, thank you. Let's take a break. [MUSIC PLAYING]
Well, now I've done this terrible thing to you. I've introduced a very complicated thing, assignment, which destroys most of the interesting mathematical properties of our programs. Why should I have done this? What possible good could this do? Clearly not a nice thing, so I better have a good excuse. Well, let's do a little bit of playing, first of all, with some very interesting programs that have assignment. Understand something special about them that makes them somewhat valuable.
Start with a very simple program which I'm going to call make-counter. I'm going to define make-counter to be a procedure of one argument n which returns as its value a procedure of no arguments-- a procedure that produces a procedure-- which sets n to the increment of n and returns that value of n.
Now we're going to investigate the behavior of this. It's a sort of interesting thing. In order to investigate the behavior, I have to make an environment model, because we can't understand this any other way. So let's just do that.
We start out with some sort of-- let's say there is a global environment that the machine is born with. Global we'll call it. And it's going to have in it a bunch of initial things. We all know what it's got. It's got things in it like say, plus, and times, and quotient, and difference, and CAR, and et cetera, lots of things. I don't know what they are, some various squiggles that are the things the machine is born with. And by doing the definition here, what I plan to do--
Well, what am I doing? I'm doing this relative to the global environment. So here's my environment pointer. In order to do that I have to evaluate this lambda expression. That means I make a procedure object.
So I'm going to make a procedure object here. And the procedure object has, as the place it's defined, the global environment. The procedure object contains some code that represents a procedure of one argument n which returns a procedure of no arguments which does something. And the define is a way of changing this environment, so that I now add to it a make-counter, a special rule for the special thing defined. But what that is, is it gives me that pointer to that procedure. So now the global environment contains make-counter as well.
Now, we're going to do some operations. I'm going to use this to make some counters. We'll see what a counter is. So let's define c1 to be a counter beginning at 0. Well, we know how to do this now, according to the model. I have to evaluate the expression make-counter in the global environment, make-counter of 0.
Well, I look up make-counter and see that it's a procedure. I'm going to have to apply that procedure. The way I apply the procedure is by constructing a frame. So I construct a frame which has a value for n in it which is 0, and the parent environment is the one which is the environment of definition of make-counter. So I've made an environment by applying make-counter to 0.
Now, I have to evaluate the body of make-counter, which is this lambda expression, in that environment. Well evaluating this body, this body is a lambda expression. Evaluate a lambda expression means make a procedure object. So I'm going to make a procedure object.
And that procedure object has the environment it was defined in being that, where n was defined to be 0. And it has some code, which is the procedure of no arguments which does something, that sets something, and returns n. And this thing is going to be the object, which in the global environment, will have the name c1. So we construct a name here, c1, and say that equals that.
Now, but also make another counter, c2 to be make-counter say, starting with 10. Then I do essentially the same thing. I apply the make-counter procedure, which I got from here, to make another frame with n being 10. That frame has the global environment as its parent. I then construct a procedure which has that as it's frame of definition. The code of it is the procedure of no arguments which does something. And it does a set, and so on. And n comes out. And c2 is this.
Well, you're already beginning to see something fairly interesting. There are two n's here. They are not one n. Each time I called make-counter, I made another instance of n. These are distinct and separate from each other. Now, let's do some execution, use those counters. I'm going to use those counters.
Well, what happens if I say, c1 at this point? Well, I go over here, and I say, oh yes, c1 is a procedure. I'm going to call this procedure on no arguments, but it has no parameters. That's right. What's its body? Well, I have to look over here, because I didn't write it down. It said, set n to one plus n and return n, increment n.
Well, the n it sees is this one. So I increment that n. That becomes one, and I return the value one. Supposing I then called c2. Well, what do I do? I say c2 is this procedure which does the same thing, but here's the n. It becomes 11. And so I have an 11 which is the value. I then can say, let's try c1 again. c1 is this, that's two, so the answer is two. And c2 gives me a 12 by the same method, by walking down here looking at that and saying, here's the n, I'm incrementing.
So what I have are computational objects. There are two counters, each with its own independent local state. Let's talk about this a little. This is a strange thing. What's an object? It's not at all obvious what an object is. We like to think about objects, because it's economical to think that way. It's an intellectual economy. I am an object. You are an object. We are not the same object.
I can divide the world into two parts, me and you, and there's other things as well, such that most of the things I might want to discuss about my workings do not involve you, and most of the things I want to discuss about your workings don't involve me. I have a blood pressure, a temperature, a respiration rate, a certain amount of sugar in my blood, and numerous, thousands, of state variables-- millions actually, or I don't know how many-- huge numbers of state variables in the physical sense which represent the state of me as a particle, and you have gazillions of them as well.
And most of mine are uncoupled to most of yours. So we can compute the properties of me without worrying too much about the properties of you. If we had to work about both of us together, than the number of states that we have to consider is the product of the number of states you have and the number of states I have. But this way it's almost a sum.
Now, indeed there are forces that couple us. I'm talking to you and your state changes. I'm looking at you and my state changes. Some of my state variables, a very few of them, therefore, are coupled to yours. If you were to suddenly yell very loud, my blood pressure would go up.
However, and it may not be always appropriate to think about the world as being made out of independent states and independent particles. Lots of the bugs that occur in things like quantum mechanics, or the bugs in our minds that occur when we think about things like quantum mechanics, are due the fact that we are trying to think about things being broken up into independent pieces, when in fact there's more coupling than we see on the surface, or that we want to believe in, because we want to compute efficiently and effectively. We've been trained to think that way.
Well, let's see. How would we know if we had objects at all? How can we tell if we have objects? Consider some possible optical illusions. This could be done. These pieces of chalk are not appropriately identical, but supposing you couldn't tell the difference of them by looking at them. Well, there's a possibility that this all a game I'm playing with mirrors. It's really the same piece of chalk, but you're seeing two of them. How would you know if you're seeing one or two? Well, there's only one way I know. You grab one of them and change it and see if the other one changed. And it didn't, so there's two of them.
And, on the other hand, there is some other screwy properties of things like that. Like, how do we know if something changed? We have to look at it before and after the change. The change is an assignment, it's a moment in time. But that means we have to know it was the same one that we're looking at. So some very strange, and unusual, and obscure, and-- I don't understand the problems associated with assignment, and change, and objects. These could get very, very bad.
For example, here I am, I am a particular person, a particular object. Now, I can take out my knife, and cut my fingernail. A piece of my fingernail has fallen off onto the table. I believe I am the same person I was a second ago, but I'm not physically the same in the slightest.
I have changed. Why am I the same? What is the identity of me? I don't know. Except for the fact that I have some sort of identity. And so, I think by introducing assignment and objects, we have opened ourselves up to all the horrible questions of philosophy that have been plaguing philosophers for some thousands of years about this sort of thing. It's why mathematics is a lot cleaner.
Let's look at the best things I know to say about actions and identity. We say that an action, a, had an effect on an object, x, 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. Let's test. It still means I have to have the x before and after. Or, the other way of saying this is, we say that two objects x and y are the same for any action which has an effect on x has the same effect on y.
However, objects are very useful, as I said, for intellectual economy. One of the things that's incredibly useful about them, is that the world is, we like to think about, made out of independent objects with independent local state. We like to think that way, although it isn't completely true.
When we want to make very complicated programs that deal with such a world, if we want those programs to be understandable by us and also to be changeable, so that if we change the world we change the program only a little bit, then we want there to be connections, isomorphism, between the objects in the world and the objects in our mental model. The modularity of the world can give us the modularity in our programming. So we invent things called object-oriented programming and things like that to provide us with that power.
But it's even easier. Let's play a little game. I want to play a little game, show you an even easier example of where modularity can be enhanced by using an assignment statement, judiciously. One thing I want to enforce and impress on you, is don't use assignment statements the way you use it in FORTRAN or Basic or something or Pascal, to do the things you don't have to do with it. It's not the right way to think for most things. Sometimes it's essential, or maybe it's essential. We'll see more about that too.
OK, let me show you a fun game here. There was mathematician by the name of Cesaro-- or Cesaro, Cesaro I suppose it is-- who figured out a clever way of computing pi. It turns out that if I take to random numbers, two integers at random, and compute the greatest common divisor, their greatest common divisor is either one or it's not one. If it's one, then they have no common divisors. If their greatest common divisor is one-- the probability that two random numbers, two numbers chosen at random, has as greatest common divisor one is related to pi.
In fact-- yes, it's very strange-- of course there are other ways of computing pi, like dropping pins on flags, and things like that, and sort of the same kind of thing. So the probability of that the GCD of number one and number two, two random numbers chosen, is 6 over pi squared. I'm not going to try to prove that. It's actually not too hard and sort of fun.
How would we estimate such probability? Well, the way we do that, the way we estimate probabilities, is by doing lots of experiments, and then computing the ratios of the ones that come out one way to the total number of experiments we do. It's called Monte Carlo, and it's useful in other contexts for doing things like integrals where you have lots and lots of variables-- the space which is limiting the dimensions you are doing you integral in.
But going back to here, Let's look at this slide, We can use Cesaro's method for estimating pi with n trials by taking the square root of six over a Monte Carlo, a Monte Carlo experiment with n trials, using Cesaro's experiment, where Cesaro's experiment is the test of whether the GCD of two random numbers-- And you can see that I've already got some assignments in here, just by what I wrote. The fact that this word rand, in parentheses, therefore, that procedure call, yields a different value than this one, at least that's what I'm assuming by writing this this way, indicates that this is not a function, that there's internal state in it which is changing. If the GCD of those two random numbers is equal to one, that's the experiment.
So here I have an experimental method for estimating the value of pi. Where, I can easily divide this problem into two parts. One is the specific Monte Carlo experiment of Cesaro, which you just saw, and the other is the general technique of doing Monte Carlo experiments. And that's what this is.
If I want to do Monte Carlo experiments with n trials, a certain number of trials, and a particular experiment, the way I do that is I make a little iterative procedure which has variable the number of trials remaining and the number trials that have been passed, that I've gotten true. And if the number remaining is 0, then the answer is the number past divided by this whole number of trials, was the estimate of the probability. And if it's not, if I have more trials to do, then let's do one.
We do an experiment. We call the procedure which is experiment on no arguments. We do the experiment and then, if that turned out to be true, we go around the loop decrementing the number of experiments we have to do by one and incrementing the number that were passed.
And if the experiment was false, we just go around the loop decrementing the number of experiments remaining and keeping the number passed the same. We start this up iterating over the total number of trials with 0 experiments past. A very elegant little program. And I don't have to just do this with Cesaro's experiment, it could be lots of Monte Carlo experiments I might do.
Of course, this depends upon the existence of some sort of random number generator. And random number generators generally look something like this. There is a random number generator-- is in fact a procedure which is going to do something just like the counter. It's going to update an x to the result of applying some function to x, where this function is some screwy kind of function that you might find out in Knuth's books on the details of programming. He does these wonderful books that are full of the details of programming, because I can't remember how to make a random number generator, but I can look it up there, and I can find out.
And then, eventually, I return the value of x which is the state variable internal to the random number generator. That state variable is initialized somehow, and has a value. And this procedure is defined in the context where that variable is bound. So this is a hidden piece of local state that you see here. And this procedure is defined in that context.
Now, that's a very simple thing to do. And it's very nice. Supposing, I didn't want to use assignments. Supposing, I wanted to write this program without assignments. What problems would I have? Well, let's see. I'd like to use the overhead machine here, thank you.
First of all, let's look at the whole thing. It's a big story. Unfortunately, which tells you there is something wrong. It's at least that big, and it's monolithic. You don't have to understand or look at the text there right now to see that it's monolithic. It isn't a thing which is Cesaro's experiment. It's not pulled out from the Monte Carlo process. It's not separated.
Let's look why. Remember, the constraint here is that every procedure return the same value for the same arguments. Every procedure represents a function. That's a different kind of constraint. Because when I have assignments, I can change some internal state variable.
So let's see how that causes things to go wrong. Well, start at the beginning. The estimate of pi looks sort of the same. What I'm doing is I take the square root of six over the random GCD test applied to n, whereas that's what this is.
But here, we are beginning to see something funny. The random GCD test of a certain number of trials is just like we had before, an iteration on the number of trials remaining, the number of trials that have been passed, and another variable x.
What's that x? That x is the state of the random number generator. And it is now going to be used here. The same random update function that I have over here is the one I would have used in a random number generator if I were building it the other way, the one I get out of Knuth's books. x is going to get transformed into x1, I need two random numbers. And x1 is going to get transformed into x2, I have two random numbers. I then have to do exactly what I did before. I take the GCD of x1 x2. If that's one, then I go around the loop with x2 being the next value of x.
You see what's happened here is that the state of the random number generator is no longer confined to the insides of the random number generator. It has leaked out. It has leaked out into my procedure that does the Monte Carlo experiment. But what's worse than that, is it's also, because it was contained inside my experiment itself, Cesaro, it leaked out of that too. Because Cesaro called twice, has to have a different value each time, if I going to have a legitimate experimental test. So Cesaro can't be a function either, unless I pass it the seed of the random number generator that is going to go wandering around.
So unfortunately, the seed of random number generator has leaked out into Cesaro, from the random number generator, that's leaked into the Monte Carlo experiment. And, unfortunately, my Monte Carlo experiment here is no longer general. The Monte Carlo experiment here knows how many random numbers I need to do the experiment.
That's sort of horrible. I lost an ability to decompose a problem into pieces, because I wasn't willing to accept the little loop of information, the feedback process, that happens inside the random number generator before that was made by having an assignment to a state variable that was confined to the random number generator. So the fact that the random number generator is an object, with an internal state variable, it's affected by nothing, but it'll give you something, and it will apply it's force to you, that was what we're missing now.
OK, well I think we've seen enough reason for doing this, and it all sort of looks very wonderful. Wouldn't it be nice if assignment was a good thing and maybe it's worth it, but I'm not sure. As Mr. Gilbert and Sullivan said, things are seldom what they seem, skim milk masquerades as cream.
Are there any questions? Are there any philosophers here? Anybody want to argue about objects? You're just floored, right? And you haven't done your homework yet. You haven't come up with a good question. Oh, well. Sure, thank you. Let's take the long break now.
This is one of over 2,200 courses on OCW. Find materials for this course in the pages linked along the left.
MIT OpenCourseWare is a free & open publication of material from thousands of MIT courses, covering the entire MIT curriculum.
No enrollment or registration. Freely browse and use OCW materials at your own pace. There's no signup, and no start or end dates.
Knowledge is your reward. Use OCW to guide your own life-long learning, or to teach others. We don't offer credit or certification for using OCW.
Made for sharing. Download files for later. Send to friends and colleagues. Modify, remix, and reuse (just remember to cite OCW as the source.)
Learn more at Get Started with MIT OpenCourseWare
MIT OpenCourseWare makes the materials used in the teaching of almost all of MIT's subjects available on the Web, free of charge. With more than 2,200 courses available, OCW is delivering on the promise of open sharing of knowledge. Learn more »
© 2001–2015
Massachusetts Institute of Technology
Your use of the MIT OpenCourseWare site and materials is subject to our Creative Commons License and other terms of use.