[MUSIC PLAYING]
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]
[MUSIC PLAYING]
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.
[MUSIC PLAYING]
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.
OK, questions?
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.