The following content is
provided under a Creative
Commons license.
Your support will help MIT
OpenCourseWare continue to
offer high quality educational
resources for free.
To make a donation, or view
additional materials from
hundreds of MIT courses, visit
MIT OpenCourseWare at
ocw.mit.edu.
PROFESSOR: Hello.
Welcome to the second
lecture in 6.01.
I hope you had a good
time last week.
Last lecture, we looked at
what is probably the most
important theme in this course,
which is, how do you
organize your thoughts, your
design, the things that you
do, in order to manage
complexity when you're trying
to build a complicated system?
The mantra for this
class is PCAP--
primitives, combinations,
abstractions, and patterns.
And last time, we saw how at
the very most elementary
level, Python provides some
tools with which we can
achieve this goal of defining
primitives, combining them
into more complicated ideas,
abstracting the important
information, and generating and
capturing new patterns.
And so, for example, we saw
that Python has a defined
statement that lets you
associate a sequence of
operations with a name--
both of those things are
important, the sequence
represents a combination, the
name represents a way that we
can abstract the behavior of the
combination and treat it
as though it were primitive
operation.
We saw that we could do the same
sort of thing for data
structures.
And in particular, the list
structure in Python allows us
to generate hierarchical
heterogeneous structures in
much the same way.
Then variables allow us to
associate names with those
structures.
And finally, we saw that classes
allow us to combine
not only data, but also
procedures --
all into one object
of related things.
So that's PCAP at the most
primitive level.
What I want to do today
is talk about
PCAP at higher levels.
How do you build upon that
framework to continue this
idea of building with
abstraction and modularity?
How do you make--
how do you combine primitive
operations into powerful
operations?
How do you combine primitive
data into powerful data?
And what I want to do is think
about the next level above the
most rudimentary level
in Python.
So we'll look first at some
programming styles, and how
that affects your ability to
define useful abstractions.
And then I'll look at something
much higher level,
which is state machines, which
is the way that we will think
about the construction
of robot controls.
So I'll start with just a few
words about how you--
the different ways that you
could structure a program.
The reason for doing this is
that the basic structure that
you use can have an important
effect on
your ability to abstract.
We'll look at three different
methodologies for
constructing a program.
I'll refer to the first one as
imperative, also procedural.
That's kind of the most basic
way you could think about
writing a program.
It's kind of a recipe.
It's kind of like cooking
with a recipe.
Take this, add this, stir this,
bake for 30 minutes,
that kind of thing.
So if you define a procedure,
if you organize the way you
think about it in terms of step
by step instructions for,
what should I do next?
We refer to that kind of an
approach as imperative.
We'll look at functional
programming.
There, even though you implement
precisely the same
algorithm, the perspective is
a little bit different.
There, rather than focusing
quite so narrowly on the step
by step, how do you get from A
to B, the idea is going to be,
think about the structure of
the problem in terms of
functions in a mathematical
sense.
By which I mean, functions
that eat inputs, generate
outputs, and don't have
side effects.
Side effects are things like
setting variables that you can
later look at.
Then I'll look at
object-oriented programming.
Again, you could be implementing
precisely the
same algorithm by using an
object-oriented approach, but
here the focus will be on
building collections of data
with procedures that are
related, and organizing the
solution to your problem in
terms of a hierarchy of such
structures.
So what I'd like to start off
today with is to look at an
example problem and how you
could program it using any of
these three approaches.
So the example program is going
to be, find a sequence
of operations--
by which I mean, an operation
is either
increment or square--
the idea is that the operations
are things that we
will do to integers.
Find a sequence of operations,
either increment or square,
that transforms one integer,
which is the initial value, i,
into a goal, which
I'll call, g.
So I want to think about
the problem of
finding such sequences.
So for example, the sequence
increment increment increment
square, when applied to
1, would give 16.
So I'm thinking about the first
increment increments 1
to give you 2.
The second increment increments
2 to give you 3.
The third increment increments
3 to give you 4.
Then square squares
4 to give you 16.
So I'll refer to this as having
found the sequence of
operations, increment increment
increment square,
that transforms 1 into 16.
Everybody with me?
OK, I'll be the judge of that.
So, to prove that you're with
me, what's the minimum length
sequence of increment and square
operations needed to
transform 1 into 100.
You've got thirty seconds to
discuss it with your neighbor,
come to an agreement, and I'm
gonna ask you to raise your
hands with a number of fingers,
(1), (2), (3), (4),
or (5), indicating
the right answer.
[AUDIENCE DISCUSSION]
PROFESSOR: OK, everybody
raise your hand.
Tell me a number of fingers
equal to the right answer,
raise your hand, show me
a number of fingers.
OK, I'm seeing--
OK.
So keep in mind the number
of fingers is the
thing before the colon.
That avoids the awkward way
of saying less than 4.
OK?
So I want the number
before the colon.
So what is the minimum
length sequence?
Raise your hand, indicate
a number of fingers.
OK, the answers are improving.
Higher so I can see you.
OK, it's about 90%
correct, I think.
OK, most people said (3), which
is another name for 5.
OK.
So how do you get
the answer 5?
What's the answer?
Somebody explain that answer?
Yeah.
AUDIENCE: Increment increment
square increment square?
PROFESSOR: That's
exactly right.
So since I have two operators,
increments and squares, and
since I'm trying to cover a big
distance, 1 to 100, square
increases faster--
at least for bigger numbers,
than increment does.
So what you'd like to do is
figure out a way of coercing
as many squares as possible.
So a good thing to do is to
start at the end, and you can
take the square root of the
first one evenly, and that
gives you 10.
But then you can't take the
square root of 10, so you back
off and you get 9, and then you
can take the square root
of 9, et cetera.
So there's any number of ways
you could solve this problem,
the point is, that there's a
simple solution which is, the
answer (3), which is
a pseudonym for 5.
So 5 operations will get
you from 1 to 100.
So what I want to do now--
now that you know the answer
to the question, I want to
write a program to
find that answer.
The most straightforward
approach that you could use is
what we would call, imperative
or procedural.
The idea is to solve the problem
by walking your way
through it in some premeditated
fashion.
A reasonable premeditated
fashion would be, think about
the sequences and order
them by length.
Think about all the sequences
of length
one, see if they work.
Think about all the sequences
of length
two, see if they work.
Three, see if they work.
And just keep going until
you find one that works.
That's a very reasonable--
that's a procedure.
Start by thinking about short
sequences, and proceed by
making them longer and longer
until you run into one that
happens to solve your problem.
So that's what's
going on here.
First I gave a name to the
operator, increment.
Then I give a name to the
operator, square--
that's just for convenience.
Then what I want to do
is write a program--
find sequence, that will start
at some initial value, say 1,
and go to some goal, say 100.
And the way I'll do that is to
enumerate over the lengths of
sequences that are possible --
1, 2, 3, et cetera.
I'll represent each one of those
sequences of operations
by this kind of a
representation, I'll make a
tuple that has a string
that tells me in
English, what did I do?
And an integer that tells
me what the answer is
after I've done that.
So looking ahead, this is the
idea-- this is the program
that I'm trying to construct.
I would like the output of the
program to start by thinking
about all sequences of length
one, then sequences of length
two, then sequences of length
three, then sequences of
length four.
For each sequence of length one,
I'd like to think about,
what are all the possible
sequences?
Well, I could start with 1 and
increment it to get 2.
Or I could start with 1 and
I could square it to 1.
That's all the possible
sequences of length one.
Then I go on to length two,
three, four, by the time I'm
down here to length four, I
could start with 1, increment
square square increment, and
that would give me 17.
That's the structure
of the program that
I'm trying to make.
Everybody with it?
So I'm going to define find
sequence, I'm going to loop
over all those lengths, I'm
going to keep track of all the
different sequences I found
as a list of tuples.
After last week you're
supposed to be very
comfortable with those
kinds of words.
Each one of the sequences is a
tuple, a string and a final
value, and the list is
all possibilities.
And I'm going to try the ones of
length one, then I'm going
to append to each one to make
sequences of two, and then I
gonna append to that to make
sequences of three, four,
five, and keep on going.
So the point is, that this is a
very simple-minded, easy to
conceive recipe for how to
search through a large number
of sequences and find the one
with the minimum length.
So when you write that program,
it iterates down
until it finds number 5, the
5-length sequence here it came
up with the answer 100,
so that's the answer.
The point is, that it was an
easy way to write the program.
We just think about telling
somebody with a pencil and
paper, what would you do?
And we tell Python, rather than
the person with the piece
of paper, what to do.
The approach is straightforward.
The only ugliness is that
it ended up with
three levels of loops.
And the most common kind of
error in this kind of
programming is you just
botch the indices.
Because it's got three nested
layers of loops, it's very
easy to lose track --
when you're thinking
of layer three --
about what's happening
in layer two.
So that's the only difficulty
in this approach.
The challenge is just to keep
all the indices consistent.
But it works.
There's nothing wrong
with this approach.
It doesn't necessarily lead to
the most modular designs, but
you can write functional
programs that work this way.
A different way to structure the
program, I'll refer it--
here's a different version of
the same program, the same
algorithm, but I'm going
to refer to this one as
functional program.
Here the idea is to focus on
procedures that implement
functions of a mathematical
type.
I want to recast the problem,
this time thinking about, how
would I divide it up
into functions
that calculate things.
So rather than focusing on,
what's the set of nested loops
that I have to make, I want
to ask, what would be a
meaningful calculation that
I would want to do
to solve this problem?
So the first thing I might do,
focusing on this part, I might
write a function, apply,
where apply --
I'm using the methodology of
functional programming --
apply is a pure function.
It's going to eat some inputs,
generate an output, and have
no side effects.
So what I want it to do, I
want to feed it a list of
functions and ask it,
what's the answer?
So apply is going to have as its
first argument a list, but
the list is going to be
a list of operations.
It's not a list of strings, it's
not a list of integers,
it's a list of functions.
And what the procedure apply is
going to do is step through
that list, applying
the functions
one by one to argument.
So the final goal is
written down here.
If I apply nothing to 7, the
answer ought to be 7.
If I apply increment to 7,
the answer ought to be 8.
If I apply square to 7, the
answer ought to be 49.
And if I apply increment
square to 7, the
answer ought to be 64.
There's a couple things you're
supposed to see here.
First off, apply is
a pure function,
it has no side effects.
It eats its input, it does a
calculation, it tells you an
answer, and it's done.
There's no extra things that's
going on behind your back,
it's not setting a variable, or
creating a list, or doing
anything like that.
It has inputs, from those inputs
it generates an output,
and that's the end
of the story.
Another thing that you should
see is that I'm treating
procedures as first-class
objects.
That's another tenet of
functional programming.
Rather than sending the input
to apply as a list of text
strings, or as a list of
integers, I'm giving it a list
of function names.
I'm treating functions just
as though it were any
other kind of data.
I'm making a list of functions
just the same as I could make
a list of integers, or if I made
a list of strings, I'm
just making a list
of functions.
So another important feature
of this program is the idea
that the functions
are being treated
as first-class objects.
The next procedure
is addLevel.
addLevel is going to be a
second pure function.
The idea there is that I'm
going to use addLevel to
construct all the different
sequences of operations that
are possible.
Each sequence is going to be
a list, so the entire--
all possible sequences will
be a list of lists.
So the idea in addLevel was
going to be that you take the
list of lists and you generate
from that list of lists a new
list of lists that has
one more element.
So if the first list of lists
had four elements in it, how
many lists are in the second
list of lists?
Five.
Four, five?
The first list of lists
represents some number of
different sequence
of operations.
My second list of lists-- what
I want to do is think about
all the different ways that
could be extended from a
length-4 sequence to a
length-5 sequence.
How many ways can you extend
a length-4 sequence to a
length-5 sequence?
Eight.
So four becomes eight.
Why does four become eight?
AUDIENCE: You've got like
two possibilities.
PROFESSOR: Because there's two
possibilities for the way that
you could extend it.
I could extend the length-4
sequence by adding on an
increment operator, or I could
extend it by adding on a
square operator.
So every time I addLevel, it's
going to create a new list of
lists with double as many
elements in it as
the old list of lists.
Does that make sense?
Other than that, the program
works very much the same--
I should illustrate that.
So addLevel applied to the
list of lists, increment.
I'm only considering one
sequence of length-1, so how
many ways can I change that if
I'm willing to add procedures
increment or square?
Well, I could end up with
increment increment by taking
that one and that one.
Or I could end up with increment
square by taking
that one and that one.
So if I were to extend the
sequence, increment, two
possible ways, by either
incrementing again or by
squaring again, I end up with
two possibilities represented
by this list of two
different lists.
Is that clear?
The important point is that I'm
treating functions as any
other kind of data element that
could be added to a list.
And when you run that program,
you get an answer that's very
similar to the program that we
looked at previously, the
imperative program.
Except that the answer now
is a list of functions.
So rather than generating a
text-string like the previous
example did, this program
generates a list as the
answer-- the program generates
a list, which
is a list of functions.
The first element in the list
is increment, the second
element in the list is the
function increment, the third
element is square increment
square.
So it gave you the same answer,
it just represented it
differently.
Here I'm representing the answer
as a list of functions.
That make sense?
So there's a number of
advantages to this approach.
One of them is that it kind of
automatically coerces you to
think about modules.
By specifically focusing my
attention on, how could I
break up that problem by
defining functions?
I end up with a bunch
of functions
here, apply and addLevel.
And being functions,
they're modules.
They are things that are easy
to manipulate in other
contexts, and in particular,
it's very easy to debug them.
Especially in an interpretive
environment like Python.
It's very easy to figure out
if your program works by
figuring out if each
module works.
That's one of the important
features of modular
programming.
When you have a module, it means
that you can use that
module in multiple
different ways.
It's easy to break apart the
debugging problem so that
rather than trying to debug
the one, monolithic
structure--
which was the procedural program
that we wrote in the
first part, here we can debug
the individual components,
which happen to be functions.
So I can ask, what happens
when I apply the
empty list to 7?
Well, my answer better be 7.
So that provides a
way of checking.
So one of the features of
thinking about the algorithm
as being broken up into a number
of functions is the
greater modularity that
allows you, for
example, easier debugging.
A much more important reason for
thinking this way though,
is the clarity of thought.
And that can be derived from
another feature of the
definition of apply.
The particular way I
defined apply is
what we call recursive.
It's recursive in the sense that
the definition of apply
calls apply.
OK, that sounds like
a bad idea, right?
How do I define something
in terms of itself?
The idea is that each
application, every time apply
calls itself--
the idea is to structure the
procedure so that the new
incarnation is in some sense,
simpler than the previous one.
If you can guarantee that, it
will reduce the complexity of
the problem from something that
you don't know how to
solve, to something that you
do know how to solve.
And for that reason, it
represents a powerful way to
think about structuring
programs.
So as an example of that--
as an example of structuring
programs as recursions, think
about raising a number to a
non-negative integer power.
So if I'm trying to raise b to
the n, if n is 0, and if n is
a non-negative integer-- well,
if n is 0, b to the n is 1.
And if n is a non-negative
integer, then b to the n can
be reduced to b times b
to the (n minus 1).
Rewriting that functionally, if
I say that my function b to
the n can be represented
by f of n.
This statement is precisely
equivalent to saying that f of
n is 1, if n is 0, or b times
f of (n minus 1) if n is
bigger than 0.
So the idea then, is that I may
not know how to raise 2 to
the 10th, but I can use the rule
to say 2 to the 10th, oh,
that must be 2 times
2 to the 9th.
Great, I don't know how to
do 2 to the 9th, either.
But 2 to the 9th is 2
times 2 to the 8th.
And eventually, I boil it down
to a case that I do know the
answer to, in particular,
b to the 0 is 1.
I would express that idea in
Python this way, define
exponent of b, n.
If n is 0, return 1, otherwise
return b times exponent of b,
(n minus 1).
OK, so what that does then
when you invoke it--
invoking exponent of 2, 6, in
fact, invokes exponent an
additional six times.
If I ask Python to evaluate
exponent of 2, 6 it will end
up calling exponent of 2, 5.
But to evaluate that, it will
call exponent of 2, 4--
3, 2, 1, 0.
Then finally, it gets
to the base case.
When it gets to the call
exponent of 2, 0 it falls into
this case which returns
1 always.
So now exponent of 2, 0 returns
1, but then that can
pick up where call exponent
of 2, 1 left off.
When I did 2, 1 it fell into
this case, where it was trying
to take 2 times exponent of
2, 0 Now it knows the
answer to 2, 0 is 1.
So I can multiply by 2 and get
the answer is 2, and it backs
out then all of the
other answers.
So that's an example of how I
could use recursion to reduce
a complicated case to a base
case-- a simple case that I
know the answer to.
Here, the recruitment with the
power of n causes a linear
increase in the number of
invocations, so we would call
that a linear process.
So the idea then is that
recursion is a good way to
reduce a complicated problem
to a simpler problem.
Now that algorithm wasn't
particularly fast.
If I imagine doing a big
number like 1024.
Raising a number to the 1024
power, that could take awhile.
How long will it take?
1024 is going to-- it will make
1024 calls to reduce it
to the base case.
Here's a way I could
speed it up.
Here I'm trying to make
something called fast
exponentiation, where I'm going
to take advantage of
another rule for the way
exponentiation works.
Not only is it true that I can
write b to the n as b times b
to the n minus 1, but if n
happens to be even, there's
another rule that I can use.
If b is even, then I can raise
b to the n over 2, and square
the answer.
OK, that's more knowledge about
the way exponents work.
The point is, that when I
think about the problem
recursively, when I think
about the problem as a
function, there's a natural way
to take advantage of that
additional information.
So what I can do then is
implement in Python in a
scheme that looks very similar
to the one that I used for the
simple exponentiation,
but it has a new
rule embedded in there.
So I can say, if n is 0 --
that was my original base
case, if n is 0
the answer is 1.
If the modulus of n when divided
by 2 is 1, if it's
odd, use the old rule--
which says b times the exponent
of b, n minus 1.
However, if it's neither 0 nor
odd, then use this new rule.
If I use that procedure compared
to the previous
program, exponent, how many
invocations of fastExponent is
generated by a call to
fastExponent of 2, 10?
So talk to your neighbor,
figure out an
answer, raise your hand.
Use the number before the dot,
just to keep you alert.
[AUDIENCE DISCUSSION]
PROFESSOR: So how many
invocations of fastExponent is
generated when you call it
with the arguments 2, 10?
Everybody raise your hand, tell
me a number of fingers.
OK, it's about 90%, something
like that.
The most common answer is five,
how did you get five?
So do you think of reducing
fastExponent of 2, 10?
What happens the first call?
AUDIENCE: It goes to
[UNINTELLIGIBLE]
divided by 2 is 5.
PROFESSOR: So the first thing
that happens, is that it
realizes that the 10 is even,
so the 10 is not 0, it's not
odd, it goes into this case.
So it tries to run it with 5.
Then when it tries to
run fastExponent
with 5, what happens?
OK, so 5 is not 0, so it's
not the base case.
It is odd, so 5 gets
reduced to 4.
4 gets reduced to 2.
2 gets reduced to--
et cetera.
So the idea is that we get a
faster convergence because,
just like in the very first
example we did, we can do two
different kinds of
operations--
either decrement, or in this
case half, and half works
faster when the numbers are
bigger than decrement does, so
it's the same idea as in
that first program.
So the idea, then, is that this
requires 5 where we would
have expected in the previous
exponent procedure, it would
have required 10.
And that difference just gets
bigger and bigger as we make n
bigger and bigger.
Much more importantly though,
than the fact that we can make
something fast, is the idea
that this is expressive.
The idea is that by structuring
the program this
way, it was perfectly obvious
how to incorporate more
knowledge about the way
exponents work.
I could have done it by using
a procedural approach.
Do this, then do this,
then do this.
But then I have very complicated
indices.
Check if it's-- check if it's
squared, check if it's
whatever, and it's inside
the loop, right?
So I complicate my looping
structure, where here--
because I'm using the functional
approach, it's very
easy to understand that it just
introduces another case.
It's nothing complicated.
So it's much more important--
the reason we think about
recursion isn't so much that
it's fast, it's because it's
expressive.
It's a way to incorporate
knowledge about an answer into
a solution, and that's
what we'd like to do.
We want to have a way of making
complicated things simply.
Here's a way to incorporate
knowledge about a procedure in
a very straightforward
fashion.
All we have is a second case.
Rather than making the loops
more complicated, all we have
is a simple new case
in this statement.
And that's especially important
when you think about
harder problems.
Here is a much harder problem.
So Tower of Hanoi, you
probably played
with this as a kid.
I want to transfer all the disks
from Post A to Post B--
I have a typo on your
handout, by the way.
I typed C on the handout,
and then I wrote the
program the other way.
That should have been a B.
Transfer a stack of disks from
Post A to Post B by moving the
disks one at a time, never
placing a bigger disk on top
of a smaller disk.
OK, that's kind of a challenging
problem.
It's a very challenging
problem if you try to
structure it as loops.
If you try to structure it as
loops I would recommend that
you do this gazillions of
times until you get the
pattern stuck in your
head, because the
pattern's not obvious.
But the pattern is obvious
if you think
about it as a recursion.
I don't know how to move n from
Post A to Post B. How
about this algorithm, take the
top n minus 1, move those to
C-- get them out of the way.
I don't know how you're going to
do that, but just trust me,
just do it.
So move n minus 1 of them off
the top, put them over on C,
that exposes the bottom one.
Move the bottom one to
B, which is where
I'd like it to be.
And then by this mysterious
process of moving n minus 1,
bring the n minus 1 back from
C back on top of B.
OK, what did I just do?
I just started with a
problem that I don't
know the answer to.
How do I move n disks from
this post to that post?
And I broke it into a sequence
of three operations, two of
which I don't know how to do,
but one of which I know.
The one will fortuitously
be the base case--
or at least similar
to the base case.
So the idea is, if I want to
transfer the n, do two
versions of the n minus 1
problem and glue them together
with this middle step.
OK, well I don't know how to
do the n minus 1 problem
either, just recurse.
How do I do the n minus 1 one?
Well, I'll do the
n minus 2 one.
I don't know how to
do that either.
Well, then do the
n minus 3 one.
Keep going until I
get it down to 1.
Because when I get down to 1,
I do know how to move 1.
Does that make sense?
This is supposed to illustrate
that the power of recursion is
in thinking.
Some algorithms are easy to
describe recursively.
That set, is the set when the
hard problem can be reduced to
a problem of the same type that
is in some sense simpler.
Here simpler means taking the
index n, and turning it
into n minus 1.
If you write this simple little
Python snippet and run
it, you get this procedure.
And if you--
so the procedure means, take the
top disk off A, put it on
B. Take the top disk off A, put
it on C. Take the top disk
off B, put it on C.
If you do that procedure, it
will magically do exactly the
right thing.
If you try to see the pattern
here, so that you were to
write this in a procedural
approach, the pattern is very
peculiar, but the algorithm for
doing it is very simple.
So the idea--
one of the reasons that we like
recursion, is this idea
of expressability.
It makes it easy to express
what we're trying to do.
OK, so that's a quick view of
the first two ways to do the
first problem that
I talked about.
We started thinking about a
sequence of operations, either
increment or square,
that transforms an
integer i into g.
The first thing we looked at
was a procedural approach
where I just gave
you a recipe--
do this, do this, do this.
The issue there is that I ended
up with nested loops
three deep.
The issue there is keeping track
of the indices for all
of the different loops.
We just talked about a
functional approach.
That can have an advantage,
especially whenever the
functional approach has a very
simple implementation.
The last approach I want to
talk about very briefly is
object-oriented approach.
Here--
I'm doing the same problem
again, find the sequence of
operations that transforms
i into g, 1 into 100.
Here I'm doing the same
algorithm again.
I mean, I haven't really changed
the algorithm in going
from the procedural approach to
the functional approach, to
here, the object-oriented
approach.
I have changed the
representation of the program,
and that's the point.
Here the representation
of the program
is in terms of objects.
Here what I'm going to do is
think about organizing all the
sequences of operations that
are possible in a tree.
So the sequences that I could
do, I could do no operation
followed by increment
increment increment.
Or I could do nothing increment
increment square.
Or I could do nothing square
increment square.
So I'm thinking about all the
possible sequences, but this
time, I'm not thinking about it
is a text-string, I'm not
thinking about it as a list of
functions that I need to call.
I'm thinking about it as this
tree, this tree of objects.
And in the object-oriented
approach, what I'm going to do
is think about the solution in
terms of building up the tree
until I find the answer.
So the key in the
object-oriented approach is to
think of a useful object.
What would I like to remember
when I'm constructing the
solution as this
kind of a tree?
Well, if I call every circle a
node, I can define a class
that will keep track of
all the important
things about each node.
And then I can structure my
program as creating nodes,
connecting them up into
that tree, and
looking for the answer.
So each one of these nodes,
each one of these circles,
ought to have a parent, they
ought to have an action, and
they ought to have an answer.
So, for example, let's say
that I started with the
initial value i equals 1.
Then this node right here
might say, whatever your
answer was at this point,
the answer here was 1.
So the answer here is going
to be 1 after it's been
incremented, so answer
should be 2.
So associated with each
one of these nodes is
going to be a parent.
The parent of this guy is this
guy, the parent of this guy is
this guy, the parent of
this guy is that guy.
Associated with each node is
going to be an action.
When I'm here, my action
is to increment.
When I'm here, my action
is to square.
And associated with each node
is going to be the answer.
So if I started at 1, by the
time I get here, the answer's
going to be 2, 4, 16.
So the idea is that I'm
structuring the
problem as a tree.
Each node has a couple things it
wants to remember, it wants
to know who it's parent was.
It wants to know, what's
the operation
associated with this node?
And it wants to know the
answer at this node.
Also associated with each
node, a method.
And the method will be to print
out the answer if this
node is the answer.
So the print routine is going to
have to say, OK, I ended up
here and that happens
to be the answer.
Well, how did I get here?
Well, I have a parent, who has
a parent, who has a parent,
who is none.
So this routine is supposed to
figure out the sequence of
operations that led me from the
initial value up at the
top to the final answer that
just happens to be goal.
Is that clear?
So then I structure the program
in terms of building
those node structures.
I start off by making a node,
which is the top node.
It has no parent, it has no
action, and the answer to the
top node is 1.
And then I just think through
loops that are very similar to
the loops that I was
using before.
They're structured similarly
to the way they were
structured in the case of the
imperative program, except now
the iterations are generating
new nodes.
The iterations are simpler,
because each one just
generates a node.
It's not like in the imperative
case where I
treated separately, what was
the ASCII string that
represented the answer,
and what was
the cumulative answer?
Here I don't have two pieces
of things that I need to do
inside my loop, I just
make a node.
So the idea is that when I solve
the problem using this
approach, what I'm trying to do
is build a representation
for the solution out
of pieces that make
sense for this problem.
So the important idea that I
want to get across was to
think about modularity at
the next higher level.
We started last time with
modularity at the most
microscopic level in Python,
and we saw how Python had
primitives that allowed us
to build more complicated
structures and simplified
our task of
understanding complex designs.
We looked at the most
primitive level
in the first lecture.
What I just went over was how
even within the structure of
Python, you can structure
programs that are
more or less modular.
And the important point is that
the way you structure the
program influences the degree
of modularity that you get.
So the structure of the program
has a significant
effect on how modular
your solution is.
What I want to do for the rest
of the time today is jump one
higher level yet.
So last time, primitives
within Python.
First half of this hour,
imperative programming,
functional programming,
object-oriented programming,
approaches that affect the
modularity of your solution.
Now I want to go up one more
level and think about
abstraction and modularity
at a much higher level.
And there, I'm going to think
about programs that control
the evolution of processes.
What's a process?
A process is some set
of procedures
that evolve over time.
An example was a bank account.
OK, with the problems that we've
been thinking about so
far, there was a clear answer.
What's the minimum-length
sequence
that blah, blah, blah.
Right?
There was a clear answer
to that problem.
Here there isn't a clear answer
to the problem, what's
the answer to the
bank account?
The bank represents a process,
it's something that
changes with time.
There are transactions.
Every time you make a deposit
or a withdrawal, you do a
transaction that causes some
action to happen in the bank.
So there isn't an answer,
there's an evolving answer.
So those are the kinds
of problems I want
to think about now.
Graphical user interfaces.
Again, there's no
answer to Excel.
Right, you can't say calculate
the answer to Excel.
Because whatever--
so the right answer for Excel
depends on, what's the
sequence of buttons that you're
pressing, and what's
the sequence of pull-down menus
that you're doing, and
all that sort of stuff.
So graphical user interfaces
are processes.
They're things that
evolve over time.
So they take a sequence of
inputs, and they generate a
sequence of outputs, just
like a bank does.
Controllers.
How do you make a
robotic steerer?
That's a process.
There are inputs--
so for example, where are you
in the middle of the lane?
What time is it?
How many pedestrians are
you about to hit?
There's all of those
kinds of inputs.
And then there is things
that you output--
which is like, keep going.
So some of the steering rules
are particularly simple.
Other steering rules are you
know, avoid the guy this--
well, anyway.
So the idea is that when you
were controlling processes,
we're going to want to think
about other kinds of
programming structures.
And even though the programming
structures are
going to be different, we're
still going to want to have
this idea of modularity.
We're going to want to be able
to think about, what are the
primitives that we have, how can
we combine them, what are
the important abstractions,
and how
do we capture patterns?
We're still going to
want to do PCAP.
But we're going to want to do
PCAP at a much higher level.
And the programming module that
I want to introduce is
the idea of a state machine.
A state machine is a very
convenient structure to
capture the behavior
of a process.
A state machine has an input,
a state, and an output.
The idea is that
it's a process.
It's operated upon in steps.
Every step there's
a new input.
From that input, the state
machine can calculate an
output and the next state.
By keeping track of the state,
this state machine can
remember everything it needs
to know so that it can
continue the process
into the future.
So the idea is that this
is going to represent a
fundamental programming element,
that we'll be able to
use to construct things that
are like processes.
And in particular, we use this
to represent the calculations
that are done within
our robot.
We'll think about the robot
as being a process.
It's not got an answer, you
can't ask the question, what's
the answer, to the robot.
But you can answer the question,
if I were trying to
steer and the sonar said
(x,y,z), how fast should you
turn the wheels?
You can think about that
as a state machine.
There's some state
of the robot.
There are some inputs--
which is what the sonars are
currently telling us, and
there's some output--
which is how fast we're
driving the wheels.
So we want to think about this
kind of a representation for
processes that evolve
over time.
OK, here's a very simple
example, a turnstile.
So we can think about the
turnstile as a state machine.
The turnstile has two states--
think about getting
onto the T--
either it's locked, or
it's not locked.
Those are the two states
of the turnstile.
There are some inputs.
The inputs are, I put a coin
in it, or I walk through it
and the turnstile turns,
or I don't do anything.
And based on those inputs, and
based on the current state,
the turnstile has
some outputs.
So the turnstile can either
allow you to enter,
or ask you to pay.
So the output might be an LED
sign that says, please enter.
Or it could have an LED sign
that says, please pay.
So those are out all of the
elements of a state machine.
And so we can think about a
graphical representation for
the turnstile in terms
of states.
Here I'm representing the
states as circles.
There are two states in the
turnstile, the turnstiles is
either locked or unlocked.
You move between states
by getting inputs.
So the inputs are represented
by the arcs--
it's the top identifier on each
arc is the input, the
bottom identifier is the output,
and the special arrow
says I start here.
So the turnstiles start out
locked in the morning--
the turnstile is locked.
If you drop a coin in it, you'll
move into the unlocked
state and it will
output enter.
It will tell you that it's OK to
enter because you've put a
coin in it now.
Then as long as you stay there
and do nothing, it'll continue
to say enter--
none enter.
Or if you keep dropping coins
in it, it'll say hey, keep
going, and it'll continue to eat
your coins and continue to
say enter--
being very nice.
So in those cases it remains
unlocked, it continues to tell
you that you can enter, and that
state persists until you
turn the turnstile.
When you turn the turnstile,
this particular turnstile
forgets how many coins you gave
it and it simply locks
and it tells you you've
got to pay more.
So the idea is that we can
capture the desired behavior
of the turnstile--
the turnstile is a process,
it's a thing that
evolves over time.
But we can capture the essence
of what we would like the
turnstile to do in terms
of this diagram.
We'll call this a
state diagram.
And then we can think about
behaviors in terms of a set of
outputs as a function of time.
So imagine that as a
function of time--
say I started in the
state locked, and
the input is nothing.
So you wake up in the morning,
you go to the T, overnight the
turnstiles have all
been locked.
They all say that they're
locked, they all say, pay, as
their output.
If I don't do anything it just
sits there and it says pay.
If I drop a coin, it says enter
and it moves into the
unlocked state.
If I don't do anything
enter persists
and it stays unlocked.
If I walk through it, it eats
the coin and it says, OK, you
gotta pay now, and it locks.
So you can think about the
evolution of this process--
the evolution of the turnstile
in terms of the diagram with
regard to time.
And the idea then is that this
is a kind of a representation
that allows us to succinctly
capture the important
information about how
processes work.
One of the important features of
this representation is that
it removes you one level away
from the looping structure.
Somewhere somebody's keeping
track of the loop.
Something's going from time 0 to
time 1, to time 2, to 3 --
we'll talk about that later.
But for the time being, this
representation focuses on what
do you do at each instance
in time?
So it's a way of tearing apart
the time part from the state
transition part.
So separate out the loop from
the essence, it's very much
like the functional example
that I did.
The functions divorce themselves
from the looping
structure that we saw in the
imperative approach, and you
could define the functions
independent of the looping.
We're doing the same
thing here.
We can define the state
machine representation
independent of the looping.
And most importantly,
this idea of
state machines is modular.
Let me show you how that
plays out when we
think about the robot.
Think about the problem that I
showed you last time where
we're trying to have the robot
go from point A to point B.
Where, for example, it knows
where it is -- say it has GPS
or something.
It knows--
from a map say, where it wants
to be, but it has no idea of
the obstacles between where it
is and where it wants to be.
So we can think about that kind
of a problem-- we looked
at this last time.
It takes a step, it
uses its sonars--
which are depicted by these
lines, to infer that there are
some walls--
that's the black dots.
So it gets a reflection off the
sonar that tells it that
there's some obstacle
in the way.
That means it's not a good
idea to try to plow
straightforward, so if I back
up, the original plan, it had
no idea what was between it and
its destination, so its
plan was just buzz
straight through.
At the end of its first step, it
has got back sonar reports
that tell it that there's a wall
here, and here, and here,
and here, and here, and here.
And based on where the wall is,
and the size of the robot,
it can't get into any of
these red squares.
It's too big.
So it knows its original path,
which went from here to here,
isn't going to work because
it can't fit through.
So it computes a new path,
and then repeat.
So the idea is that
we're solving a
very complicated problem--
the robot 's solving a very
complicated problem, and what
I'd like to do now is
think through a
solution to that problem.
As the robot's moving along,
on every step it's getting
some new input--
it's behaving just like
a state machine.
On every step it gets new
input from the sonars.
Based on that new
input, it knows
where there's new obstacles.
Based on that knowledge,
it has a better idea of
what the map is.
Based on that knowledge, it has
a better idea of finding a
plan that might work.
And then based on that
knowledge, it knows how to
tell the wheels to
move forward.
So think about where
it is right now.
It has done, logically, three
different things.
It has figured out where
the walls are.
That's the thing represented
in black and red.
We'll call that --
it made a map.
Given the map, it
made a plan--
that's the blue thing.
Given the plan, it figured out
how to turn the wheels.
It's going straight ahead.
So there are logically three
things going on,
all at the same time.
You can imagine writing an
imperative program to capture
that behavior, but that's
complicated.
Not impossible, you
would structure it
as a bunch of loops.
You know, it would--
unless you're a crack
programmer, and
I'm sure you are--
it will end up being
an ugly program.
The idea is, that by using the
state machine representation
we'll be able to think
about how to pull
apart those three pieces.
So in particular, we can think
about the state machine for
the robot--
which would take the sensory
input from the sonars and turn
it into an action, which
is turning the wheels--
we can think about that as
having three modules.
There's the mapmaker module,
which just looks at the
sensory input.
Doesn't care what the plan is,
doesn't care how it's turning
the wheels right now.
All it the cares about
is, what was the --
what's my current state?
And what new information
can I infer about the
map from the sonars?
So given the sonars and my
current state, what's the most
recent version of the map
likely to look like.
Then there's a planner that
says, given the sonars and the
map, how would I construct
this blue line?
Given my current understanding
of the map of the world, how
would I get from where I am,
to where I want to be?
That's a plan.
Then from that plan--
say the robot is here, from the
plan you can see that the
thing the robot should do
is move straight ahead.
So the mover can take
the heading--
the heading is just simply the
coordinates of the first
destination point in the plan.
So the mover can look at the
heading, the first destination
point, and figure out how
to make the wheels turn.
So the idea is that the state
machine representation will
let us parse a very complicated
problem.
A process--
the process by which the robot
gets from here to there, a
very complicated process.
We're going to be able to
break it up into pieces.
The pieces are going
to be much easier.
They're going to be modular.
We will be able to write this
piece, and debug it and make
sure that it all works,
independent
of these other pieces.
Because we just have it do any
arbitrary walk around task or
no walk around at all and ask
whether the set of sensory
inputs generates the
correct map.
Similarly, this will
be a module.
This will be something that we
can debug as well without
having the other
pieces working.
We'll be able to say, OK, well,
what if I fed a map that
I hand constructed
to the robot--
ignore this, just forget
this, assume
there is none of these--
I can hand make a map, feed it
to this guy, give it sensory
input and see if it comes up
with the right heading.
It's just like in that
functional approach example
that I did.
Having made the individual
functions, we were able to
test them individually because
they were modules.
Having made these individual
state machines, we'll be able
to test them individually
because they're modules.
Then at the end we'll be able to
paste them all together and
get a very complicated
behavior--
much more complicated than you
would expect from the sum of
the three programming
activities.
That's what we want.
That's how we want to
structure things.
We want to be able to combine
simple things and get
something that seems more
powerful than the linear
combination, because it is.
Okay, so that's the goal.
To do that, we're going to
invent some new Python
structures.
And we're going to think about
the representation of the
state machine in Python and
we will use objects.
In fact, we use objects
at three levels.
We'll think about the highest
level of abstraction of this
problem as a state machine.
So there's going to be
state machine class.
The state machine class is going
to have all the things
in it that all state machines
have to know about.
All state machines, as we define
them, are going to have
to know how to start, how to
make one step, and how to
combine single steps into a
sequence of steps-- and we'll
call that operation
of transduction.
So if you give me a list of
inputs, transduce will make a
list of outputs.
So all state machines are
going to be able to--
all state machines are going to
have to know how to start,
step, and transduce.
Then for particular types
of state machines--
like the turnstile, we will
generate a subclass.
The subclass of turnstiles are
going to have to know-- all of
those are going to have to
do some other things.
All turnstiles are going to have
to have a rule for what
happens when you're in state
A and you get input i--
what should you do?
Well there's going to be a
general pattern that all
turnstiles will subscribe to.
We'll put those in the
subclass, turnstile.
Then individual turnstiles.
The first one in the Kendall
stop, the second one in the
Kendall stop, the first one in
the Central stop, the third
one in the Harvard stop--
particular turnstiles
will be instances of
the turnstile class.
So the way this plays out, we'll
have a generic state
machine class defined here.
The state machine class will
have a method, start.
Start will create the instance
variable, state.
Every instance of a turnstile
has to remember where it is,
that's an instance variable.
That instance variable gets
created by the start routine.
So the very first thing you do
if you want to talk about a
new turnstile, is you have to
instantiate the turnstile.
Then you have to make
an instance--
and you do that by
calling start.
Every state machine has to
have a start routine.
Every start routine will do
this-- it'll create an
instance variable.
Every state machine has to
know how to take a step.
We will make a generic method
called, getNextValues, which
will include all of the rules
for how the class turnstile
generates a new output, and a
new state from the implant.
And every subclass of state
machine will have to define
what one of those looks like.
The important thing here is that
every state machine has
to have a step routine, and
the way we've decided to
implement the step routine is
to call the getNextValues,
that is particular to the
subclass of interest.
So for example, the
getNextValues for a turnstile,
might be different from the
getNextValues for a planner or
a for a mapper.
Then we will subclass state
machine according to the kind
of state machine we're making.
A planner, a mapper, a
mover, a turnstile.
So a turnstile will be a class,
which is a subclass of
state machine, and that's where
we'll define how do you
do getNextValues.
If you're a turnstile, here's
the way you ought to do
getNextValues.
When I call getNextValues, I'll
tell you your current
state and the current input, and
from those two you ought
to be able to figure out the
next state and the output.
So the getNextValues method for
the turnstile class ought
to take state and input--
the input, and the current
state, and it generates a
tuple, which is the new
state and the output.
And this is the rule by
which it happens.
All turnstiles also have
the same start state.
So if you look back again at
state machine, state machine
created the instance
variable, state, by
looking at start's state.
All turnstiles have the
same start state,
they all start locked.
And then finally, the way we'll
use this, is we will
instantiate a turnstile, we
will make an instance of
turnstile, put it into TS,
turnstile, so this could be
Central Square 16,
for example.
And then that instance, we'll
be able to do things like
start, step, and transduce.
So if I transduced this the
input, none, coin, none, turn,
turn, coin, coin, on a
particular turnstile, I'll
generate some sequence
of outputs like so.
So that's a complicated example
using turnstiles.
Let me do something simpler
just to make
sure you get the idea.
Let's think about a simpler
class, a subclass of state
machines, a subclass
called accumulator.
That subclass always has a start
state of 0, and has a
getNextValues that returns the
same value for the next state
and for the output.
It always adds the current
state to the input--
hence the name accumulator.
It accumulates the input by
taking the current state,
adding the currently input
to generate a new state--
that's just like
a bank account.
The bank account would
accumulate transactions.
So if the input to the
accumulator is 7, then the
state gets incremented by 7, and
the output is the value of
the new state.
So that's what this says, the
starting state is always 0,
the getNextValues always returns
for this new state--
state plus input, and for the
current output, the same thing
as the current state.
Everybody's clear?
So the question is, what would
happen if I did this sequence
of operations?
Let's say that I have the state
machine class, I have
the accumulator subclass.
I make an instance A, I do some
stuff to A, I make an
instance B. I do some stuff
to B, I do some stuff to
A, and I type this.
What gets printed?
Take 30 seconds, talk to your
neighbor, figure out
what the answer is.
[AUDIENCE DISCUSSION]
PROFESSOR: OK.
What will get printed by
the print statement?
Answer (1), (2), (3),
(4), or (5).
Everybody raise your hand
with an answer.
Excellent, excellent,
almost 100%.
I don't think I see a
single wrong answer.
So everybody seems
to be saying (2).
(2), OK, so what's going on?
So A is an accumulator, we
start it, we step it.
B is an accumulator, we start
it, we step it, we step.
So what is going to be A,
how did you get five?
Well, A started at 0 that's
what the start
method does, right?
Stepping it stepped it to 7.
B made a new one, we can ignore
that because it's a
separate instance.
The idea is that the state is
associated with the instance.
So when we perturb the state of
B by doing B.step, we are
touching the state of A--
because they're instance
variables.
So that means when we
decrement it, we're
decrementing the 7 to get 5.
OK, seems reasonable.
So the answer is either
(1), or (2), or none.
Why is it the same (21, 21) (21,
21), when we called the
A.getNextValues, and here we're
calling the B. Why are
they the same?
AUDIENCE: Because it sets
the state [INAUDIBLE].
PROFESSOR: So getNextValues
is a pure function.
So it gets passed in the state,
the first number is the
state, the second number
is the input.
So it doesn't pay any attention
to the instance
variable state, it uses the
thing they got passed in.
Furthermore, the getNextValues
associated with A, is
precisely the same as the
getNextValues associated with
B, because getNextValues is
something that's in the
accumulator subclass.
So they're exactly the same
because it's the same method.
So what should be in your
head, is a picture
that looks like this.
When we said create a state
machine, that was something we
put in our library, and that
makes this environment.
When we said subclass of state
machine to make accumulator,
that made a new class,
a new environment.
And then when we made A and
B by saying that A equals
accumulator, and B equals
accumulator, that made instances.
The instances were initially
empty, but when I say A.start,
that created the state variable
in the A instance,
and in the B instance, and
associated values with them.
So with the picture that you're
supposed to have in
mind, is that there's an
environment associated with
state machine.
There's a separate environment
associated with accumulator,
but just one of those.
But there's two instances.
So there's two sets of instance
variables, one
associated with A and B. So
the answer was number (2).
Now the robot example was
supposed to motivate this idea
that what we're going to do
with state machines is put
them together modularly.
So what we're going to do-- and
you start to do that this
week, and also continuing into
next week, we'll figure out
how to compose state machine
M1 and M2 in a cascade.
That means the output of the
first one becomes the input to
the second one.
We'll put things together in
parallel, we'll put things
together with feedback.
So what we'll do, is we'll
figure out operators.
PCAP--
primitives, means
of combination,
abstraction, patterns.
The primitives are going to be
the state machines, the ways
we combine them are going to be
these kinds of structures.
Cascade them, put them in
parallel, that kind of stuff.
And that's going to allow us to
make complicated brains out
of simple ones.
Here's a very simple example,
what if I had a state machine
A, which is an accumulator,
so A is an instance of
accumulator.
B is an instance of accumulator,
there's two
separate instances.
And C is a new state composed
by cascading A and B. What
would happen if I typed out
C.transduce so ([7, 3, 4])?
What's the answer to C.transduce
([7,3,4])?
[AUDIENCE DISCUSSION]
PROFESSOR: So what happens
when I transduce C?
Well C is a composition of A
and B. In particular, it's
this composition.
C is just the cascade of A into
B. What that means is
that if I put some sequence of
inputs into the first machine,
and if I put some sequence of
the inputs into C, it's the
same as putting it into A.
The output of the composite
machine is the output of B.
And the input to B is the same
as the output of A. So the
([7,3,4]) goes through A, which
is an accumulator, and
comes out [7, 10, 14].
It accumulates, that's
what accumulators do.
That clear?
Then [7, 10, 14]
goes into B and comes
out [7, 17, 31].
So C, which is the cascade of
two accumulators, ends up
transforming [7, 3, 4]
into [7, 17, 31].
So that's the idea for how
we're going to compose
complicated behaviors out of
simple ones, and that's what
the assignment is
for this week.
