Topics covered: Compound Data
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.
PROFESSOR: Well, so far in this course we've been talking about procedures, and then just to remind you of this framework that we introduced for talking about languages, we talked about the primitive things that are built into the system. We mentioned some means of combination by which you take the primitive things and you make more complicated things. And then we talked about the means of abstraction, how you can take those complicated things and name them so you can use them as simple building blocks.
And then last time you saw we went even beyond that. We saw that by using higher order procedures, you can actually express general methods for computing things. Like the method of doing something by fixed points, or Newton's method, and so the incredible expressive power you can get just by combining these means of abstraction.
And the crucial idea in all of this is the one that we build a layered system. So for instance, if we're writing the square root procedure, somewhere the square root procedure uses a procedure called good-enough, and between those there is some sort of abstraction boundary. It's almost as if we go out and in writing square root, we go and make a contract with George, and tell George that his job is to write good-enough, and so long as good-enough works, we don't care what it does. We don't care exactly how it's implemented. There are levels of detail here that are George's concern and not ours. So for instance, George might use an absolute value procedure that's written by Harry, and we don't much care about that or even know that, maybe, Harry exists.
So the crucial idea is that when we're building things, we divorce the task of building things from the task of implementing the parts. And in a large system, of course, we have abstraction barriers like this at lots, and lots, and lots of levels. And that's the idea that we've been using so far over and over in implementing procedures.
Well, now what we're going to do is look at the same issues for data. We're going to see that the system has primitive data. In fact, we've already seen that. We've talked about numbers as primitive data. And then we're going to see their means of combination for data. There's glue that allows you to put primitive data together to make more complicated, kind of compound data. And then we're going to see a methodology for abstraction that's a very good thing to use when you start building up data in terms of simpler data.
And again, the key idea is that you're going to build the system in layers and set up abstraction barriers that isolate the details at the lower layers from the thing that's going on at the upper layers. The details at the lower layers, the ideas, they won't matter. They're going to be George's concern because he signed this contract with us for how the stuff that he implements behaves, and how he implements the thing is his problem.
All right, well let's look at an example. And the example I'm going to talk about is a system that does arithmetic on rational numbers. And what I have in mind is that we should have something in the computer that allows us to ask it, like, what's the sum of 1/2 and 1/4, and somehow the system should say, yeah, that's 3/4. Or we should be able to say what's 3/4 times 2/3, and the system should be able to say, yeah, that's 1/2. Right? And you know what I have in mind. And you also know how to do this from, I don't know, fifth grade or sixth grade.
There are these formulas that say if I have some fraction which is a numerator over a denominator, and I want to add that to some other fraction which is another numerator over another denominator, then the answer is the numerator of the first times the denominator of the second, plus the numerator of the second times the denominator of the first. That's the numerator of the answer, and the denominator is the product of the two denominators.
Right? So there's something from fifth or sixth grade fraction arithmetic. And then similarly, if I want to multiply two things, n1 over d1 multiplied by n2 over d2 is the product of the numerators over the product of the denominators.
So it's no problem at all, but it's absolutely no problem to think about what computation you want to make in adding and multiplying these fractions. But as soon as we go to implement it, we run up across something. We don't have what a rational number is. So we said that the system gives us individual numbers, so we can have 5 and 3, but somehow we don't have a way of saying there's a thing that has both a 3 and a 4 in it, or both a 2 and a 3. It's almost as if we'd like to imagine that somehow there are these clouds, and a cloud somehow has both a numerator and a denominator in it, and that's what we'd like to work in terms of.
Well, how are we going to solve that problem? We're going to solve that problem by using this incredibly powerful design strategy that you've already seen us use over and over. And that's the strategy of wishful thinking. Just like before when we didn't have a procedure, we said, well, let's imagine that that procedure already exists. We'll say, well, let's imagine that we have these clouds.
Now more precisely what I mean is let's imagine that we have three procedures, one called make-RAT. make-RAT is going to take as arguments two numbers, so I'll call them numerator and denominator, and it'll return for us a cloud-- one of these clouds. I don't really know what a cloud is. It's whatever make-RAT returns, that's its business.
And then we're going to say, suppose we've got one of these clouds, we have a procedure called numer, which takes in a cloud that has an n and a d in it, whatever a cloud is, and I don't know what it is, and returns for us the numerator part. And then we'll assume we have a procedure denom, which again takes in a cloud, whatever a cloud is, and returns for us the denominator [? required. ?]
This is just like before, when if we're building a square root, we assume that we have good enough. Right? And what we'll say is, we'll go find George, and we'll say to George, well, it's your business to make us these procedures. And how you choose to implement these clouds, that's your problem. We don't want to know.
Well, having pushed this task off onto George, then it's pretty easy to do the other part. Once we've got the clouds, it's pretty easy to write the thing that does say addition of rational numbers. You can just say define, well, let's say +RAT. Define +RAT, which will take in two rational numbers, x and y. x and y are each these clouds.
And what does it do? Well, it's going to return for us a rational number. What rational number is it? Well, we've got the formulas there. The numerator of it is the sum of the product of the numerator of x and the denominator of y. It's one thing in the sum. And the other thing in the numerator is the product of the numerator of y and the denominator of x. The star, close the plus.
Right, that's the first argument to make-RAT, which is the numerator of the thing I'm constructing. And then the rest of the thing goes into make-RAT is the denominator of the answer, which is the product of the denominator of x and the denominator of y. Like that. OK? So there is the analog of doing rational number addition. And it's no problem at all, assuming that we have these clouds.
And of course, we can do multiplication in the same way. Define how to get the product of two rational numbers, call it *RAT. Takes in two of these clouds, x and y, it returns a rational number, make-RAT, whose numerator is the product of the numerators-- numerator of x times the numerator of y. And the denominator of the thing it's going to return is the product of the denominators.
Well, except that I haven't told you what these clouds are, that's all there is to it. See, what did I do? I assumed by wishful thinking that I had a new kind of data object. And in particular, I assumed I had ways of creating these data objects. Make-RAT creates one of these things. This is called a constructor. All right, I have a thing that constructs such data objects. And then I assume I have things that, having made these things, I have ways of getting the parts out. Those are called selectors.
And so formally, what I said is I assumed I had procedures that are constructors and selectors for these data objects, and then I went off and used them. That's no different in kind from saying I assume I have a procedure good-enough, and I go use it to implement square root.
OK, well before we go on, let's ask the question of why do we want to do this in the first place? See, why do we want a procedure like +RAT that takes in two rational numbers and returns a rational number? See, another way to think about this is, well, here's this formula. And I've also got to implement something that adds rational numbers.
One other way to think about is, well, there's this thing, and I type in four numbers, an n1, and a d1, and an n2, and a d2. And it sets some registers in the machine to this numerator and this denominator. So I might say, well, why don't I just add rational numbers by I type in four numbers, numerators and denominators, and get out two numbers, which is a numerator and a denominator.
Why are we worrying about building things like this anyway? Well, the answer is, suppose you want to think about expressing something like this, suppose I'd like to express the idea of taking two rational numbers, x plus y, say, and multiplying that by the sum of two other rational numbers. Well, the way I do it, having things like +RAT and *RAT, is I'd say, oh yeah, what that is is just the product. That's *RAT of the sum of x and y and the sum of s and t.
So except for syntax, I get an expression that looks like the way I want to think about it mathematically. I want to say there are two numbers. There's a thing which is the sum of them, and there's a thing which is the sum of these two. That's this and this. And then I multiply them. So I get an expression that matches this expression.
If I did the other thing, if I said, well, the way I want to think about this is I type into my machine four numbers, which are the numerators and the denominators of x and y, and then four more numbers, which are the numerators and denominators of s and t. And then what I'd be sitting with is, well, what would I do? I'd add these, and somehow I'd have to have two temporary variables, which are the numerators and denominators of this sum, and I'd go off and store them someplace.
And then I'd go over here, I'd type in four more numbers, I'd get two more temporary variables, which are the numerators and denominators of s and t. And then finally, I put those together by multiplying them. You see, what's starting to happen, there are all these temporary variables, which are sort of the guts of the internals of these rational numbers that start hanging out all over the system.
And of course, if I had more and more complicated expressions, there'd be more and more guts hanging out that confuse my programming. And those of you who sort of programmed things like that, where you're just adding numbers in assembly language, you sort of see you have to suddenly be concerned with these temporary variables.
But more importantly than confusing my programming, they're going to confuse my mind. Because the whole name of this game is that we'd like the programming language to express the concepts that we have in our heads, like rational numbers are things that you can add and then take that result and multiply them.
Let's break for questions. Yeah?
AUDIENCE: I don't quite see the need- when we had make-RAT with the numerator and denominator, we had to have the numerator and denominator to pass as parameters to create the cloud, and then we extracted to get back what we had to have originally.
PROFESSOR: That's right. So the question is, I sort of have the numerator and the denominator, why am I worrying about having the cloud given that I have to get the pieces out? That's sort of what I tried to say at the end, but let me try and say it again, because that's really the crucial question.
The point is, I want to carry this numerator and denominator around together all the time. And it's almost as if I want to know, yeah, there's a numerator and denominator in there, but also, I would like to say, fine, but from another point of view, that's x. And I carry x around, and I name it as x, and I hold it. And I can say things like, the sum of x and y, rather than just have-- see, it's not so bad when I only think about x, but if I have a system with 10 rational numbers, suddenly I have 20 numerators and denominators, which are not necessarily-- if I don't link them, then it's just 20 arbitrary numbers that are not linked in any particular way.
It's a lot like saying, well, I have these instructions that are the body of the procedures, why do I want to package them and say it's the procedure? It's exactly the same idea.
No? OK. Let's break, let's just stretch and get somebody-- [INAUDIBLE]
OK, well, we've been working on this rational number arithmetic system, and then what we did, the important thing about what we did, is we thought about the problem by breaking it into two pieces. We said, assume there is this contract with George, and George has figured out the way to how to construct these clouds, provided us procedures make-RAT, which was a constructor, and selectors, which are numerator and denominator. And then in terms of that, we went off and implemented addition and multiplication of rational numbers.
Well, now let's go look at George's problem. How can we go and package together a numerator and a denominator and actually make one of these clouds? See, what we need is a kind of glue, a glue for data objects that allows us to put things together. And Lisp provides such a glue, and that glue is called list structure. List structure is a way of gluing things together, and more precisely, Lisp provides a way of constructing things called pairs.
There's a primitive operator in Lisp called cons. We can take a look at it. There's a thing called cons. Cons is an operator which takes in two arguments called x and y, and it returns for us a thing called a pair. All right, so a thing called a pair that has a first part a second part.
So cons takes two objects. There's a thing called a pair. The first part of the cons is x, and the second part of the cons is y. And that's what it builds. And then we also assume we have ways of getting things out. If you're given a pair, there's a thing called car, and car of a pair, p, gives you out the first part of the pair, p. And there's a thing called cdr, and cdr of the pair, p, gives you the second part of the pair, p. OK, so that's how we construct things.
There's also a conventional way of drawing pictures of these things. Just like we write down that as the conventional way of writing Plato's idea of two, the way we could draw a diagram to represent cons of two and three is like this. We draw a little box. And so here's the box we're talking about, and this box has two arrows coming out of it. And say the first part of this pair is 2, and the second part of this pair is 3. And this notation has a name, it's called box and pointer notation.
By the way, let me say right now that a lot of people get confused that there's some significance to the geometric way I drew these pointers, the directions. Like some people think it'd be different if I took this pointer and turned it up here, and put the 3 out here. That has no significance. All right? It's merely you have a bunch of arrows, these pointers, and the boxes. The only issue is how they're connected, not the geometric arrangement of whether I write the pointer across, or up, or down.
Now it's completely un-obvious, probably, why that's called list structure. We're not actually going to talk about that today. We'll see that next time.
So those are pairs, there's cons that constructs them. And what I'm going to know about cons, and car, and cdr, is precisely that if I have any x and y, all right, if I have any things x and y, and I use cons to construct a pair, then the car of that pair is going to be x, the thing I put in, and the cdr of that pair is going to be y. That's the behavior of these operators, cons, car, and cdr.
Given them, it's pretty clear how George can go off and construct his rational numbers. After all, all he has to do-- remember George's problem was to implement make-RAT, numerator, and denom. So all George has to do is say define make-RAT of some n and a d-- so all I have to do is cons them. That's cons of n and d.
And then if I want to get the numerator out, I would say define the numerator, numer, of some rational number, x. If the rational number's implemented as a pair, then all I have to do is get out the car of x. And then similarly, define the denom is going to be the cdr, the other thing I put into the pair.
Well, now we're in business. That's a complete implementation of rational numbers. Let's use it. Suppose I want to say, so I want to think about how to add 1/2 plus 1/4 and watch the system work. Well, the way I'd use that is I'd say, well, maybe define a. I have to make a 1/2. Well, that's a rational number with numerator 1 and denominator 2, so a will be make-RAT of 1 and 2.
And then I'll construct the 1/4. I'll say define d to be make-RAT of 1 and 4. And if I'd like to look at the answer-- well, assuming I don't have a special thing that prints rational numbers, or I could make one-- I could say, for instance, define the answer to be +RAT of a and b, and now I can say, what's the answer? What are the numerators and denominators of the answer?
So if I'm adding 1/2 and 1/4, I'll say, what is the numerator of the answer? And the system is going to type out, well, 6. Bad news. And if I say what's the denominator of the answer, the system's going to type out 8. So instead of what I would really like, which is for it to say that 1/2 and 1/4 is 3/4, this foolish machine is going to say, no, it's 6/8.
Well, that's sort of bad news. Where's the bug? Why does it do that, after all? Well, it's the way that we just had +RAT. +RAT just took the-- it said you add the numerator times the denominator, you add that to the numerator times the denominator, and put that over the product of the two denominators, and that's why you get 6/8.
So what was wrong with our implementation of +RAT? What's wrong with that rational number arithmetic stuff that we did before the break? Well, the answer is one way to look at it is absolutely nothing's wrong. That's perfectly good implementation. It follows the sixth grade, fifth grade mathematic for adding fractions.
One thing we can say is, well, that's George's problem. Like, boy, wasn't George dumb to say that he can make a rational number simply by sticking together the numerator and the denominator? Wouldn't it be better for George, when he made a rational number, to reduce the stuff to lowest terms? And what I mean is, wouldn't it be better for George, instead of using this version of make-RAT, to use this one on the slide? Or instead of just saying cons together n and d, what you do is compute the greatest common divisor of n and d, and gcd is the procedure which, well, for all we care is a primitive, which computes the greatest common divisor of two numbers.
So the way I can construct a rational number is get the greatest common divisor of the two numbers, and I'm going to call that g, and then instead of consing together n and d, I'll divide them through. I'll cons together the quotient of n by the the gcd and the quotient of d by the gcd. And that will reduce the rational number to lowest terms. So when I do this addition, when +RAT calls make-RAT-- and for the definition of +RAT it had a make-RAT in there-- just by the fact that it's constructing that, the thing will get reduced to lowest terms automatically.
OK, that is a complete system. For rational number arithmetic, let's look at what we've done. All right, we said we want to build rational number arithmetic, and we had a thing called +RAT. We implemented that. And I showed you multiplying rational numbers, and although I didn't put them up there, presumably we'd like to have something that subtracts rational numbers, and I don't know, all sorts of things. Things that test equality in division, and maybe things that print rational numbers in some particular way.
And we implemented those in terms of pairs. These pairs, cons, car, and cdr that are built into Lisp. But the important thing is that between these and these, we set up an abstraction barrier. We set up a layer of abstraction.
And what was that layer of abstraction? That layer of abstraction was precisely the constructor and the selectors. This layer was make-RAT, and numer, and denom. This methodology, another way to say what it's doing, is that we are separating the way something is used, separating the use of data objects, from the representation of data objects. So up here, we have the way that rational numbers are used, do arithmetic on them. Down here, we have the way that they're represented, and they're separated by this boundary. The boundary is the constructors and selectors.
And this methodology has a name. This is called data abstraction. Data abstraction is sort of the programming methodology of setting up data objects by postulating constructors and selectors to isolate use from representation. Well, so why? I mean, after all, we didn't have to do it this way. It's perfectly possible to do rational number addition without having any compound data objects, and here on the slide is one example.
We certainly could have defined +RAT, which takes in things x and y, and we'll say, well what are these rational numbers really? So really, they're just pairs, and the numerator's the car and the denominator's the cdr. So what we'll do is we'll take the car of x times the cdr of y, multiply them. Take the car of y times the cdr of x, multiply them. Add them. Take the cdr of x and the cdr of y, multiply them, and then constitute together. Well, that sort of does the same thing.
But this ignores the problem of reducing things to lowest terms, but let's not worry about that for a minute. But so what? Why don't we do it that way? Right? After all, there are sort of fewer procedures to define, and it's a lot more straightforward. It saves all this self-righteous BS about talking about data abstraction. We just sort of do it. I mean, who knows, maybe it's even marginally more efficient depending on whatever compiler were using for this.
What's the point of isolating the use from the representation? Well, it goes back to this notion of naming. Remember, one of the most important principles in programming is the same as one of the most important principles in sorcery, all right? That's if you have the name of the spirit, you get control over it.
And if you go back and look at the slide, you see what's in there is we have this thing +RAT, but nowhere in the system, if I have a +RAT and a -RAT and a *RAT, and things that look like that, nowhere in the system do I have a thing that I can point at which is a rational number. I don't have, in a system like that, the idea of rational number as a conceptual entity.
Well, what's the advantage of that? What's the advantage of isolating the idea of rational numbers as a conceptual entity, and really naming it with make-RAT, numerator, and denominator. Well, one advantage is you might want to have alternative representations. See, before I showed you that one way George can solve this things not reduced to lowest terms problem, is when you build a rational number, you divide up by the greatest common denominator.
Another way to do that is shown over here. I can have an alternative representation for rational numbers where when you make a rational number, you just cons them. However, when you go to select out the numerator, at that point you compute the gcd of the stuff that's sitting in that pair, and divide out by the gcd. And similarly, when I get the denominator, at that point when I go to get the denominator, I'll divide out by the gcd.
So the difference would be in the old representation, when ans was constructed here, say what's 6 and 8, in the first way, the 6 and 8 would have got reduced when they got stuck into that pair, numerator would select out 3. And in the way I just showed you, well, ans would get 6 and 8 put in, and then at the point where I said numerator, some computation would get done to put out 3 instead of 6. So those are two different ways I might do it.
Which one's better? Well, it depends, right? If I'm making a system where I am mostly constructing rational numbers and hardly ever looking at them, then it's probably better not to do that gcd computation when I construct them. If I'm doing a system where I look at things a lot more than I construct them, then it's probably better to do the work when I construct them. So there's a choice there.
But the real issue is that you might not be able to decide at the moment you're worrying about these rational numbers. See, in general, as systems designers, you're forced with the necessity to make decisions about how you're going to do things, and in general, the way you'd like to retain flexibility is to never make up your mind about anything until you're forced to do it.
The problem is, there's a very, very narrow line between deferring decisions and outright procrastination. So you'd like to make progress, but also at the same time, never be bound by the consequences of your decisions. Data abstraction's one way of doing this. What we did is we used wishful thinking. See, we gave a name to the decision. We said, make-RAT, numerator, and denominator will stand for however it's going to be done, and however it's going to be done is George's problem.
But really, what that was doing is giving a name to the decision of how we're going to do it, and then continuing as if we made the decision. And then eventually, when we really wanted it to work, coming back and facing what we really had to do. And in fact, we'll see a couple times from now that you may never have to choose any particular representation, ever, ever. Anyway, that's a very powerful design technique. It's the key to the reason people use data abstraction. And we're going to see that idea again and again. Let's stop for questions.
AUDIENCE: What does this decision making through abstraction layers do to the axiom of do all your design before any of your code?
PROFESSOR: Well, that's someone's axiom, and I bet that's the axiom of someone who hasn't implemented very large computer systems very much. I said that computer science is a lot like magic, and it's sort of good that it's like magic. There's a bad part of computer science that's a lot like religion. And in general, I think people who really believe that you design everything before you implement it basically are people who haven't designed very many things.
The real power is that you can pretend that you've made the decision and then later on figure out which one is right, which decision you ought to have made. And when you can do that, you have the best of both worlds.
AUDIENCE: Can you explain the difference between let and define?
PROFESSOR: Oh, OK. Let is a way to establish local names. Let me give you sort of the half answer. And I'll say, later on we can talk about the whole very complicated thing. But the big difference for now is that, see, when you're typing at Lisp, you're typing in this environment where you're making definitions. And when you say define a to be 5, if I say define a to be 5, then from then on the thing will remember that a is 5.
Let is a way to set up a local context where there's a definition. So if I type something like, saying let a-- no, I shouldn't say a-- if I said let z be 10, and within that context, tell me what the sum of z and z is. So if I typed in this expression to Lisp, and then this would put out 20. However, then if I said what's z, the computer would say that's an unbound variable.
So let is a way of setting up a context where you can make definitions. But those definitions are local to this context. And of course, if I'd said a in here, I'd still get 20. But this a would not interfere at all with this one. So if I type this, and then type this, and then say what's a? a will still be 5. So there's some other subtle differences between let and define, but that's the most important one.
All right, well, we've looked at implementing this little system for doing arithmetic on rational numbers as an example of this methodology of data abstraction. And that's a way of controlling complexity in large systems. But, see, like procedure definition, and like all the ways we're going to talk about for controlling complexity, the real power of these things show up not when you sort of do these things in themselves, like it's not such a great thing that we've done rational number arithmetic, it's that you can use these as building blocks for making more complicated things.
So it's no wonderful idea that you can just put two numbers together to form a pair. If that's all you ever wanted to do, there are tons of ways that you can do that. The real issue is can you do that in such a way so that the things that you build become building blocks for doing something even more complex? So whenever someone shows you a method for controlling complexity, you should say, yeah, that's great, but what can I build with it?
So for example, let me just run through another thing that's a lot like the rational number one. Suppose we would like to represent points in the plane. You sort of say, well, there's a point, and we're going to call that point p. And that point might have coordinates, like this might be the point 1 comma 2. The x-coordinate might be 1, and it's y-coordinate might be 2. And we'll make a little system for manipulating points in the plane.
And again, we can do that-- here's a little example of that. It can represent vectors, the same as points in the plane, and we'll say, yep, there's a constructor called make-vector, make-vector's going to take two coordinates, and here we can implement them if we like as pairs, but the important thing is that there's a constructor. And then given some vector, p, we can find its x-coordinate, or we can get its y-coordinate. So there's a constructor and selectors for points in the plane.
Well, given points in the plane, we might want to use them to build something. So for instance, we might want to talk about, we might have a point, p, and a point, q, and p might be the point 1, 2, and q might be the point 2, 3. And we might want to talk about the line segment that starts at p and ends at q. And that might be the segment s. So we might want to build points for vectors in terms of numbers, and segments in terms of vectors. So we can represent line segments in exactly the same way.
All right, so the line segment from p to q, we'll say there's a constructor, make-segment. And make up names for the selectors, the starting point of the segment and the ending point of the segment. And again, we can implement a segment using cons as a pair of points, and car and cdr get out the two points that we put together to get the segment.
Well, now having done that, we can have some operations on them. Like we could say, what's the midpoint of a line segment? So here's the midpoint of a line segment, that's going to be the points whose coordinates are the averages of the coordinates of the endpoints. OK, there's the midpoint.
So to get the midpoint of a line segment, s, we'll just say grab the starting point to the segment, grab the ending point of the segment, and now make a vector-- make a point whose coordinates are the average of the x-coordinate of the first point and the x-coordinate of the second point, and whose y-coordinate is the average of the y-coordinates. So there's an implementation of midpoint.
And then similarly, we can build something like the length of the segment. The length of the segment is a thing whose-- use Pythagoras's rule, the length of the segment is the square root of the d x squared plus d y squared. We'll say to get the length of a line segment, we'll let dx be the difference of the x-coordinate of one endpoint and the x-coordinate of the other endpoint, and we'll let dy be the difference of the y-coordinates. And then we'll take the square root of the sum of the squares of dx and dy, that's what this says. All right, so there's an implementation of length.
And again, what we built is a layered system. We built a system which has, well, say up here there's segments. And then there's an abstraction barrier. The abstraction barrier separates the implementation of segments from the implementation of vectors and points, and what that abstraction barrier is are the constructors and selectors. It's make-segment, and segment-start, and segment-end.
And then there are vectors. And vectors in turn are built on top of pairs and numbers. So I'll say pairs and numbers. And that has its own abstraction barrier, which is make-vector, and x-coordinate, and y-coordinate. So we have, again, a layered system. You're starting to see that there are layers here.
I ought to mention, there is a very important thing that I kind of took for granted. And it's sort of so natural, but on the other hand it's a very important thing. Notice that in order to represent this segment s, I said this segment is a pair of points.
And a point is a pair of numbers. And if I were going to draw the box and pointers structure for that, I would say, oh, the segment is, given those particular representations that I showed you, I'd say this segment s is a pair, and the first thing in the pair is a vector, and the vector is a pair of numbers. And that's this, that's p. And the other thing in the segment is q, which is itself a pair of numbers.
So I almost took it for granted when I said that cons allows you to put things together. But it's very easy to not appreciate that, because notice, some of the things I can put together can themselves be pairs. And let me introduce a word that I'll talk about more next time, it's one of my favorite words, called closure. And by closure I mean that the means of combination in your system are such that when you put things together using them, like we make a pair, you can then put those together with the same means of combination. So I can have not only a pair of numbers, but I can have a pair of pairs.
So for instance, making arrays in a language like Fortran is not a closed means of combination, because I can make an array of numbers, but I can't make an array of arrays. And one of the things that you should ask, one of your tests of quality for a means of combination that someone shows you, is gee, are the things you make closed under that means of combination? So pairs would not be nearly so interesting if all I could do was make a pair of numbers. I couldn't build very much structure at all.
OK, well, we'll come back to that. I just wanted to mention it now. You'll hear a lot about closure later on.
You can also see the potential for losing control of complexity as you have a layered system if you don't use data abstraction. Let's go back and look at this slide for length. Length works and is a simple thing because I can say, when I want to get this value, I can say, oh, that is the x-coordinate of the first endpoint of the segment. And each of these things, each of these selectors, x-coordinate and endpoint, stand for a decision choice whose details I don't have to look at.
So I could perfectly well, again, just like rational numbers I did before, I could say, oh well, gee, a segment really is a pair of pairs. And the x-coordinate of the first endpoint or the segment really is the-- well, what is it? It's the car of the car of the segment. So I could perfectly well go and redefine length. I could say, define the length of some segment s.
And I could start off writing something like, well, we'll let dx be-- well, what's it have to be? It's got to be the difference of the two coordinates, so that's the difference of, the first one is the car of the car of s, subtracted from the first one, the car of the other half of it, the cdr of s. All right, and then dy would be-- well, let's see, I'd get the y-coordinate, so it'd be the difference of the cdr of the car of s, and the cdr of the cdr of s, sort of go on.
You can see that's much harder to read than the program I had before. But worse than that, suppose you'd gone and implemented length? And then the next day, George comes to you and says, I'm sorry, I changed my mind. I want to write points with the x-coordinate first. So you come back you stare at this code and say, oh gee, what was that? That was the car, so I have to change this to cdr, and this is cdr, and this now has to be car. And this has to be car.
And you sort of do that, and then the next day George comes back and says, sorry, the guys designing the display would like lines to be painted in the opposite direction, so I have to write the endpoint first in the order. And then you come back and you stare at this code, and say, gee, what was it talking about? Oh yeah, well I've got to change this one to cdr, and this one becomes car, this one comes car, and this becomes cdr.
And you go up and do that, and then the next day, George comes back and says, I'm sorry, what I really meant is that the segments always have to be painted from left to right on the screen. And then you sort of, it's clear, you just go and punch George in the mouth at that point. But you see, as soon as we have a 10 layer system, you see how that complexity immediately builds up to the point where even something like this gets out of control.
So again, the way we've gotten out of that is we've named that spirit. We built a system where there is a thing, which is the representation choice for how you're going to talk about vectors. And choices about that representation are localized right there. They don't have their guts spilling over into things like how you compute the length and how you compute the midpoint. And that's the real power of this system. OK, we're explicit about them, so that we have control over them. All right, questions?
AUDIENCE: What happens in the case where you don't want to be treating objects in terms of pairs? For instance, in three-dimensional space, you'd have three coordinates. Or even in the case where you have n-dimensional space, what happens?
PROFESSOR: Right, OK. Well, this is a preview of what I'll say tomorrow. But the point is, once you have two things, you have as many things as you want. All right? Because if I want to make three things, I could start making things like a pair whose first thing is 1, and whose second thing is another pair that, say, has 2 and 3 in it. And so on, a hundred things. I can nest them out of pairs.
I made a pretty arbitrary decision about how to do it, and you can immediately see there are lots of ways to do that. What we'll start talking about next time are conventions for how to do things like that. But notice that what this really depends on is I can make pairs of pairs. If all I could do was make pairs of numbers, I'd be stuck.
OK. Let's break.
All right, well, we've just gone off and done a couple of simple examples of data abstraction. Now I want to do something more complicated. We're going to talk about what it means. And this will be harder, because it's always much harder in computer programming to talk about what something means than to go off and do it.
But let's go back to almost the very beginning. Let's go back to the point where I said, we just assumed that there were procedures, make-RAT, and numer, and denom. Let's go back to where we had this, at the very beginning, constructors and selectors, and went off and defined the rational number arithmetic. And remember, I said at that point we were sort of done, except for George. Well, what is it that we'd actually done at that point? What was it that was done?
Well, what I want to say is, what was done after we'd implemented the operations and terms of these, was that we had defined a rational number representation in terms of abstract data.
What do I mean by abstract data? Well, the idea is that at that point, when we had our +RAT and our *RAT, that any implementation of make-RAT, and numerator, and denominator that George supplied us with, could be the basis for a rational number representation. Like, it wasn't our concern where you divided through to get the greatest common denominator, or any of that.
So the idea is that what we built is a rational arithmetic system that would sit on top of any representation. What do I mean by any representation? I mean, certainly it can't be the case that all I mean is George can reach in a bag and pull out three arbitrary procedures and say, well, fine, now that's the implementation. That can't be what I mean.
What I've got to mean is that there's some way of saying whether three procedures are going to be suitable as a basis for rational number representation. If we think about it, what suitable might mean is if I have to assume something like this, I have to say that if x is the result of say, doing make-RAT of n and d, then the numerator of x divided by the denominator of x is equal to n over d.
See, what that is is that's George's contract. What we mean by writing a contract for rational numbers, if you think about it, this is the right thing. And the two ones we showed do the right thing. See, if I'm taking out greatest common divisors, it doesn't matter whether I take them out or not, or the place where I take them, because the idea is I'm going to divide through.
But see, this is George's contract. So what we really say to George is your business is to go off and find us three procedures, make-RAT, and numerator, and denominator, that fulfill this contract for any choice of n and d. And that's what we mean by we can use that as the basis for a rational number representation. And other than that, it fulfills this contract. We don't care how he does it. It's not our business. It's below the layer of abstraction.
In fact, if we want to say, what is a rational number really? See, what's it really, without having to talk about going below the layer of abstraction, what we're forced into saying is a rational number really is sort of this axiom, is three procedures, make-RAT, numerator, and denominator, that satisfy this axiom. In some sense, abstractly, that's what a rational number is really.
That's sort of easy words to listen to, because what you have in your head, of course, is well, for all this thing about saying that's what a rational number is really, you actually just saw that we built rational numbers. See, what we really did is we built rational numbers on top of pairs. So for all I'm saying abstractly, we can say a rational number really is just this axiom. You can listen to that comfortably, because you're saying, well, yeah, but really it's actually pairs, and I'm just annoying you by trying to be abstract.
Well, let me, as an antidote for that, let me do something that I think is really going to terrify you. I mean, it's really going to bring you face to face with the sort of existential reality of this abstraction that we're talking about. And what I'm going to talk about is, what are pairs really? See, what did I tell you about pairs? I tricked you, right?
I said that Lisp has this primitive called cons that builds pairs. But what did I really tell you about? If you go back and said, let's look on this slide, all I really told you about pairs is that there happens to be this property, these properties of cons, car, and cdr. And all I really said about pairs is that there's a thing called cons, and a thing called car, and a thing called cdr.
And it is the case that if I build cons of x, y and take car of it, I get x. And if I build cons of x, y and get cdr of it, I get y. And even though I lulled you into thinking that there's something in Lisp that does that, so you pretended you knew what it was, in fact, I didn't tell you any more about pairs than this tells you about rational numbers. It's just some axiom for pairs.
Well, to drive that home, let me really scare you, and show you what we might build pairs in terms of. And what you're going to see is that we can build rational numbers, and line segments, and vectors, and all of this stuff in terms of pairs, and we're going to see below here that pairs can be built out of nothing at all. Pure abstraction.
So let me show you on this slide an implementation of cons, car, and cdr. And we'll look at it again in a second, but notice that their procedure definitions of cons, car, and cdr, you don't see any data in there, what you see is a lambda. So cons here is going to return-- is a procedure that returns a procedure, just like AVERAGE DAMP.
Cons of a and b returns a procedure of an argument called pick, and it says, if pick is equal to 1, I'm going to return a, and if pick is equal to 2, I'm going to return b, and that's what cons is going to be. Car of a thing x, car of a pair x, is going to be x applied to 1. And notice that makes sense. You might not understand why or how I'm doing such a thing, but at least it makes sense, because the thing constructed by cons is a procedure, and car applies that to 1.
And similarly, cdr applies that thing to 2. OK, now I claimed that this is a representation of cons, car, and cdr, and notice there's no data in it. All right, it's built out of air. It's just procedures. There's no data objects at all in that representation. Well, what could that possibly mean? Well, if you really believe this stuff, then you have to believe that in order to show that that's a representation for cons, car, and cdr, all I have to do is show that it satisfies the axiom.
See, all I should have to convince you of is, for example, that gee, that car of cons of 37 and 49 is 37 for arbitrary values of 37 and 49. And cdr the same way. See, if I really can demonstrate to you that that weird procedure definition, in terms of [? air ?], has the property that it satisfies this, then you just have to grant me that that is a possible implementation of cons, car, and cdr, on which I can build everything else.
Well, let's look at that. And this will be practice in the substitution model. How could we check this? We sort of know how to do that. It's just the same substitution model. Let's look. We start out, and we say, what's car of cons of 37 and 49? What do we do? Cons is some procedure. Its value is cons was a procedure of a and b. The thing returned by cons is its procedure body with 37 and 49 substituted for the parameters. It'll be 37 substituted for a and 49 substituted for b.
So this expression has the same meaning as this expression. Its car of, and the body of cons was this thing that started with lambda. And it says, so if pick is equal to 1, where pick is this other argument, if pick is equal to 1, it's 37, that's where a was, and if pick is equal to 2, it's 49. So that's the first step. I'm just going through mechanical substitution. And remember, at this point in the course, if you're confused about what things mean, go mechanically through the substitution model.
Well, what is this reduced to? Car said, take your argument, which in this case is this, and apply it to 1. That was the definition of car. So if I look at car, if I do that, the answer is, well, it's that argument, this was the argument to car, applied to 1. Well, what does that mean? I take 1, and I substitute it in the body here for this value of pick, which is the name of the argument, what do I get? Well, I get the thing that says if 1 equals 1 it's 37, and if 1 equals 2 it's 49, so the answer's 37. And similarly, if I'd taken cdr, that would apply it to 2, and I'd get 49.
So you see, what I've demonstrated is that that completely weird implementation of cons, car, and cdr, satisfies the axioms. So it's a perfectly valid way of building, in fact, all of the data objects we're going to see in Lisp. So they all, if you like, can be built on sort of existential nothing. And as far as you know, that's how it works. You couldn't tell. If all you're ever going to do with pairs is construct them with cons and look at them with car and cdr, you couldn't possibly tell how this thing works.
Now, it might give you a sort of warm feeling inside if I say, well, yeah, in fact, for various reasons there happens to be a primitive called cons, car, and cdr, and if it's too scary, if this kind of stuff is too scary, you don't have to look inside of it. So that might make you feel better, but the point is, it really could work this way, and it wouldn't make any difference to the system at all. So in some sense, we don't need data at all to build these data abstractions. We can do everything in terms of procedures.
OK, well, why did I terrify you in this way? First, I really want to reinforce this idea of abstraction, that you really can do these things abstractly. Secondly, I want to introduce an idea we're going to see more and more of in this course, which is we're going to blur the line between what's data and what's a procedure.
See, in this funny implementation it turned out that cons of something happened to be represented in terms of a procedure, even though we think of it as data. While here that's sort of a mathematical trick, but one of the things we'll see is that a lot of the very important programming techniques that we're going to get to sort of depend very crucially on blurring this traditional line between what you consider a procedure and what you consider data. We're going to see more and more of that, especially next time.
AUDIENCE: If you asked the system to print a, what would happen?
PROFESSOR: The question is, what would happen if I asked the system to print a. Given this representation, you already know the answer. The answer is compound procedure a, just like last time. It'd say compound procedure. It might say a little bit more. It might say compound procedure lambda or something or other, depending on details of how I named it. But it's a procedure.
And the only reason for that is I haven't told the system anything special about how to print such things. Now, it's in fact true that with the actual implementation of cons that to be built in the system, it would print something else. It would print, say, this is a pair.
AUDIENCE: When you define cons, and then you pass it into values, how does it know where to look for the cons, because you can use cons over and over again? How does it know where to look to know which a and b it's supposed to pull back out? I don't know if I'm expressing that quite right. Where is it stored?
PROFESSOR: OK, the question is, I sort of have a cons with a 37 and a 49, and I might make another cons with a 1 and a 2, and I might have one called a, and I might have one called b. And the question is, how does it know? And why don't they get confused? And that's a very good question. See, you have to really believe that the procedures are objects.
It's sort of like saying-- let's try another simpler example. Suppose I ask for the square root of 3. So I asked for the square root of 5, and then I ask for the square of 20. You're probably not the least bit bothered that I can take square root and apply it to 5, and then I can take square root and apply it to 20. And there's sort of no issue, gee, doesn't it get confused about whether it's working on 5 or 20? There's no issue about that because you're thinking of a procedure which goes off and does something.
Now, in some sense you're asking me the same question. But it's really bothering you, and it's bothering you for a really good reason. Because when I write that, you're saying gee, this is, I know, sort of a procedure. But it's not a procedure that's just running. It's just sort of a procedure sitting there. And how can it be that sometimes this procedure has 37 and 49, and there might be another one which has 5 and 6 in there, and why don't they get confused?
So there's something very, very important that's bothering you. And it's really crucial to what's going on. We're suddenly saying that procedures are not just the act of doing something. Procedures are conceptual entities, objects, and if I built cons of 37 and 49, that's a particular procedure that sits there. And it's different from cons of 3 and 4. That's another procedure that sits there.
AUDIENCE: Both of them exist independently.
PROFESSOR: And exists independently.
AUDIENCE: And they both can be referenced by car and cdr.
PROFESSOR: And they both would be referenced by car and cdr. Just like I could increment this, and I could increment that. They're objects. And that's sort of where we're going. See, the fact that you're asking the question shows that you're really starting to think about the implications of what's going on. It's the difference between saying a procedure is just the act of doing something. And a procedure is a real object that has existence.
AUDIENCE: So when the procedure gets built, the actual values are now substituted for a and b--
PROFESSOR: That's right.
AUDIENCE: And then that procedure exists as lambda, and pick is what's actually passed in.
PROFESSOR: Yes, when cons gets called, and the result of cons is a new procedure that's constructed, that new procedure has an argument that's called pick.
AUDIENCE: But it no longer has an a and b. The a and b are the actual values that are passed through.
PROFESSOR: And it has-- right, according to the substitution model, what it now has is not those arbitrary names a and b, it somehow has that 37 and 49 in there. But you're right, that's a hard thing to think about it, and it's different from the way you've been thinking about procedures.
AUDIENCE: And if I have again cons of 37 and 49, it's a different object?
PROFESSOR: And if you make another cons of 37 and 49, you're into a wonderful philosophical problem, which is going to be what the lecture about halfway through this course is about. Which is, if I cons 37 and 49, and I do it again, is that the same thing, or is it a different thing? And how could you tell? And when could it possibly matter?
And that's sort of like saying, is that the same thing as this? Or is this the same thing as that? It's the same kind of question. And that's a very, very deep question. And I can't answer in less than an hour. But we will.