Leslie Kaelbling: 6.01 Lecture 01
PROFESSOR KAELBLING: OK, so welcome to the first lecture of 601. You guys have all had the first real class of 601 already, but we'll have lectures on Monday mornings. I'm Leslie Kaelbling. I'm the lecturer.
You should also make a point of getting to know your section instructors. I'm not going to introduce them now, but be sure when you go to your section, you know who your instructors are. They're going to try to learn your names, and you should try to learn theirs.
These lectures are going to be recorded, so I want to let you know that. If you have privacy concerns-- we don't have any cameras facing the audience. There's only a camera facing here.
But if somebody asks a question, it'll get recorded. It won't have your name under it or anything, so you shouldn't let that inhibit you. But if you do have a worry about that, you can contact me and we'll figure out something to do about that. That was a required disclaimer.
Next week, starting next week, there's two interesting things about next week. First of all, next week, Tuesday is a Monday. So we will have lecture, but it will be on Tuesday. And it will be in 32-123. OK? So it's a better room. So that's the plan for next week.
OK. So let me also say I've been recording these little mini-lectures which have a lot of sort of nitty-gritty technical detail. And I'll use those for things like showing you technique and working through problems and so on. And I'm going to use the lecture time partly for that, but partly also to try to do a better job of connecting the pieces, setting the stage, explaining why we're doing things, and so on.
I love questions, harassment, comments, cheers, songs, whatever-- anything you're willing to do to keep us awake in the morning. So don't hesitate to ask a question or make a comment. If it bothers me, I'll tell you, but it almost certainly won't.
So, OK. So what is EECS I about? What are we doing here in 601? The idea here is to illustrate for you a bunch of really important things about the enterprise of electrical engineering and computer science. These are things that we'll hit several times in the context of the course and that you'll see over and over again if you continue on in this discipline.
One of the most important things is how it is that we can design really complicated things. So the kinds of systems that EEs make, and the kinds of systems that computer scientists make, and now the kinds of systems that we make together are some of the most complicated systems that anybody's ever tried to make. They have enormous numbers of pieces and parts that all have to work together in a good way.
And if anybody, no matter how smart they are, tried to conceive of the whole system at once, they wouldn't get it. So the only way that we can design a really complicated thing is to come up with systems for designing small pieces and for understanding how they fit together. So that's going to be our overriding theme throughout this whole class, is to see lots and lots of systems where we can take small pieces, define them, and combine them to make a big and complicated system.
Another important theme is modeling and controlling physical systems. And doing this with computational or electronic systems mixed in. So you've all learned a lot of math. Lots of people have learned a lot of math.
You've solved an enormous number of problems from the back of a text book, right? Chapter 6.2.3, 6.2.4, you do all these problems. You know that if they're in a chapter about integration by parts, there'll be problems about integration by parts.
But that math, most of the math you've learned is actually also good for something. And another of our themes is to use mathematical tools to make models of systems that let us actually make analysis and predictions of those systems without necessarily building them first. So building things and trying them out is one way to understand how they're going to work. But mathematical modeling and analysis is a way to do some of that work up front, so that we only build the things that we think probably have a pretty good chance of working. So we're going to be about taking mathematical tools and applying them to actual problems to give us some leverage in engineering.
Another aspect of what we're going to think about is building systems that work effectively when the world is uncertain. So sometimes it's not so hard to design a system that will work under assumptions of perfectness. Everything is a perfect size and shape. We know exactly where the robot's positioned. We know a bunch of things exactly.
But the fact is that the real world is never like that. Stuff isn't perfect. There's always a little noise, a little slop. Nobody's quite sure about something.
And so we're going to think about how it is that the systems we build can either be robust or adaptive to uncertainty in the world around them. That's going to be another big important theme.
We're going to do this work in the context of mobile robots. So we have 50 of these little rolling red robots, and we'll use them in lab. And the idea is that we will use these robots to help exercise these concepts. This isn't a class about robotics, and robots aren't the main emphasis, but they're a good way to get an illustration of a lot of these principles in the physical world.
So some people say, oh, 601, it's so broad, it must be shallow. And I think that that's not true. I think it's pointwise deep.
So we do cover a lot of territory. We're going to talk about software engineering, about sort of control theory, signals and systems, about circuits, probability, planning. So that's a lot of topics. But we're not going to do them superficially. We're going to take some particular core samples in different places of these fields to help you understand what the ideas are and how those things might appear and apply in the future work that you would do.
So something about the pedagogical style. As you can tell already from the schedule, there's less emphasis on lecture and more emphasis on lab. We think that you finally really learn stuff when you actually sit down and try to do it. And also, that that's the moment when you're most susceptible to explanation.
So we have these labs, we have software labs and design labs with a lot of staff, and the staff are there to help you figure things out when you need help figuring them out. So we're not going to leave you to languish in your dorm room at 11:00 at night to try to figure out the hardest concept. We hope that you'll wrestle with it with us in lab. So that's the style.
And we can set up, what we'd like to do is have you try to do something. When you try to do something and it doesn't quite work, then you're ready to be taught some theory about how it might really work. And then you can go back and apply the theory with some appreciation. So that's the sort of pedagogical plan of the course.
OK. So the first module, which we've already started, is about software engineering. And again, the emphasis is going to be on abstraction and modularity of several different kinds. Since we've already kind of dug into that and we're going to spend more time on it later today, I'm not going to really talk about that in too much detail now.
The second module is on signals and systems. It's about how we can build models of the interaction between a system and its environment and study the properties of that whole thing together. So an example here is, imagine that we have a robot, and it's job-- or a car. Could be you.
Car's trying to steer, and it would like to go down the center of the line. Now that's not really a good way to drive your car. I hope you don't try to go down the line. You should stay to the right-hand side of the line, anyway. But imagine that your goal is to drive down a line.
And so here's the car and it's over here to the right-hand side of the line, and you say oh, well, gosh, I'm over to the right, so it feels like I should probably steer to the left. OK, so that's good. So you steer to the left and then you're there. And that seems good. And you're still to the right of the line, so you think maybe you should steer to the left.
But now you're where you wanted to be, and so you think, well, maybe I should try to steer so I'm straight ahead. So you start to correct. But now you're over on that side, so now you've got to steer to the right.
So is this going to work out as a strategy for driving? No. Why not? Well, some people drive like that. OK. It's true. It's true. We've all been behind somebody who's driving like that. But it might be better not to.
What's the issue? How come this thing is not driving so well? What should it do differently? Where was the crucial sort of mistake?
STUDENT: It didn't correct early enough.
PROFESSOR LESLIE KAELBLING: It didn't correct early enough. Right. So actually, let me go back here. So right about there, did it need to keep turning hard to the left? No. It was going to be OK, right? It should have just kind of chilled out, waited till it got to the line. OK. So it's kind of trying too hard.
So we're going to make mathematical models of systems that try too hard like this and see how that comes out, and then see how we can fix it. That's just an example. That's a good example.
We're also going to do a section on circuits. So many of you, in some version of some physics class-- or several, several times, several versions of several physics classes-- have done stuff with circuits. Most of you-- it used to be that most people when they arrived at MIT had already built a radio. How many of you have built a radio?
OK, good. So not most of you. But that's good. Good for you who have. So most of us now haven't done anything with circuits before we come to MIT? I was actually a philosophy major as an undergraduate. I certainly hadn't done anything with circuits. So you see, you can recover from all kinds of mistakes in your youth.
[LAUGHTER]
That's good. Great. So I'm not a philosopher anymore. I'm a recovering philosopher. OK.
So we're going to look at pretty simple circuits. But we're actually going to build them and see what they do. And we're going to study how to design them, see how composition and abstraction in circuits is really different than composition and abstraction programs, in a really interesting and fundamental way.
And then we're going to use these circuits to build the control system. we're going to make the head of this robot track light and make that actually make the robot-- eventually, we're going to build up to having a pet robot that will follow you around if you have a flashlight. And we're going to study how to build that system so that it's reliable and stable and it doesn't have weird steering properties like our previous example.
So yeah. This is circuit board. This is a beautiful circuit board built by Denny Freeman, our beautiful circuit board-builder instructor and who also designed the head hardware. We also have cool new head hardware this year, so it's even better than that. Your circuit boards should all look like that. It's a thing of beauty, to be admired. So just remember that. OK.
The last section is going to be in probability and planning. So, so far, we have been building models of how a [INAUDIBLE] system interacts with the world so that we can prove we, as the engineers, can analyze the system and say, oh yeah, this system is well-designed. It's going to go straight down the center of the road. Or oh, this system, it's a swervy one.
In the last module, we're going to think about, actually, in some sense, putting the analysis in the head of the system itself. So now the robot is going to think about, should I drive this way or should I drive that way? What would happen if I drove this way versus that way?
And it's going to build probabilistic models of what it knows about the world, based on the sensor readings that it has. So that's using modeling and analysis again, but this time, putting the modeling and analysis inside the head of the robot. And the goal here, in particular, is to build systems that are robust in the face of significant uncertainty about the world around them.
OK. So that was the introductory matter. So that's the first part of what I wanted to do today. The second part will be a sort of lightning story about the stuff we did last week, and then we'll get into some new material for this week.
So last week we looked at-- and I'm going to trust you, right? So I'm posting reading assignments and little mini-lectures, and you should read and watch them in some combination. You probably don't have to read it all and watch it all both. But it's on you to assimilate that material and be sure you understand it and ask questions on Piazza.
So 190 of you have already gone to Piazza, which is this website. There's a link on every page on our website. And it's a place to ask and answer questions. And lots of people have already asked and answered questions.
You can help answer the questions, which is excellent. I love it when students answer. We'll answer, too. And it's a great place to go to look things up if you're having trouble with a problem. Maybe there's somebody who's already asked a question about it. It's a great place to ask us questions, and so on.
So I really want you to be sure to go to Piazza and use it. I really want you to also take responsibility for reading stuff in the readings, which is basically a book, and/or watching these lectures on tape or live, and/or watching the other lectures. OK.
So last week, what we studied was the Python interpreter, and we looked at various kinds of programming in a particular object-oriented programming strategy. And so that was meant to illustrate a theme which will come up over and over again in the class. So I just want to kind of talk that through.
So in Python, there are expressions, right? Everybody knows this by now. You knew this from arithmetic. You can write 3 times 8, 3 times 8 minus 2. So 3 is an expression, 3 times 8 is an expression, 3 times 8 minus 2 is an expression.
And the good thing about a system like this, like that nice algebraic way of writing down numerical expressions, is that any expression can be substituted for another expression in a formula. That you can take 3 times 8, you could evaluate it, you could turn it into 24. Any place you have 24, you could have 3 times 8. Any place you have 3 times 8, you could have 24.
So these systems derive their meaning by having individual pieces with meaning, like 3, and the times symbol, and by having ways of combining things. So we know that a number and a times symbol and another number can be combined to make another number. So the system is compositional. There's an infinity of algebraic expressions like this that you could write down.
We're familiar with lots of other compositional systems. Natural language is compositional, which is good for us, right? We can write down sentences. We can understand sentences that we've never heard before.
And the way we understand sentences is because we can assign some semantics or meaning to the individual pieces, and then we can make a meaning of the whole out of the meanings of the pieces. When we can't, it's an idiom or a joke or a confusion or something else, right? It's a surprising feature of language. The normal feature of language is that the semantics are compositional.
We don't have to say-- "Sentence 942," I could say to you, and you'd have to, hmm, go look up in a book, sentence 942. Oh, she said the lecture tomorrow will be somewhere else. Right? It's not like that. And good thing, right?
So composition is key to making complicated things. It's the simple pieces that have a meaning by themselves and ways of putting them together. So in Python now, we have a way of capturing patterns of composition. So now not only can we take the individual pieces and put them together, but we can take the stereotyped ways of putting them together and give them a name so we can use them over and over again.
So this is the act of defining a procedure. This is something you're all used to doing, right? If we have written 2 times 2 and then 3 times 3 and then maybe 8 plus 4 times 8 plus 4, we should be getting tired, now, of writing something times something. And say, you know what? I do that a lot. I'm going to give it a name-- square. And then I don't have to write that anymore.
So a theme in finding common patterns and abstracting them is that it actually makes the description of the thing that you're doing often shorter. And it usually decreases the number of errors. So if ever you're writing a program and you find yourself cutting and pasting lines of text into your program and changing them slightly, it's almost certain that you need a procedure or something. That you need to abstract away from that thing that you're doing over and over and over again and find a way to define it and then use it.
And so of course, the great thing about procedures is you could define a procedure. It would be really bad if you could just define a procedure and then only use it, but not build it into another procedure. But of course, because our system is compositional, when you define a procedure, you can use it anywhere else that you would have used a primitive procedure. And so that's what lets you make a huge, infinitely varied set of possible programs.
OK. So object-oriented programming is another sort of variation on the theme of taking simple things and putting them together to make more complicated things. Objects and classes give us a way putting data and procedures together in sort of new ways and in ways that let us, again, simplify and abstract. We're always going to be about writing short, simple, clear, abstract descriptions of what we're doing.
So in object-oriented programming, classes give us a way to put data and procedures together. And you've seen how this works, already. Instances-- so you can define a class that has some attributes and some procedures in it that are common to the members of that class. We make instances, and instance, they're particular individuals that inherit the data items and the procedures that are in the class, but can also have specific values of their own. So this is a simple example-- there are lots of them in the notes-- where individual instances have different values of attributes, but there's some shared stuff in a class above them.
We can also have subclasses. So we'll go through an example later today, in more detail. A subclass, right? We might define a class-- Student601, here-- and so that's a class. And we might say, well, that's a subclass of students.
So maybe there's somethings that are totally generic about all students. They have student IDs and they have a place where they live, and all that kind of stuff. They have a tuition bill.
But maybe 601 students are special, right? We might say that their lecture day is on Tuesday. Well, that's actually true next week. So I remind you. We have a lecture time. We might have a way of calculating scores, and so on.
So maybe those methods and those attributes are different for different subclasses of students. But by defining class Student601 as a subclass of student, I don't have to go and copy all that stuff out of the regular student class and put it in my Student601 class. I can just inherit it.
Hey you guys have a question? Ask me a question. Ask me a questions. No, please.
STUDENT: He just wants to ask, where's lecture?
PROFESSOR LESLIE KAELBLING: Where's lecture? OK. I'll talk to you about that later. 32-123. OK. So that's the deal.
So we save copying code. We save-- once you copy stuff, if you fix a bug in one place, you duplicate it somewhere else. Other people have trouble understanding it. We save that by inheriting things hierarchically from less specific types of objects in our world.
OK. So that's this idea. Now we have this idea called PCAP, which we'll allude to now and then. PCAP stands for Primitives, means of Combination, ways of Abstracting, and Patterns that we use to put these things together.
So for Procedures, we have primitive procedures, like plus and times. We have ways of putting them together, like if or while statements or function composition. We have ways of abstracting, of building what feels like a new primitive, like def. And we have these patterns of assembling more complicated pieces that we'll see later on in the context of Python as higher-order procedures. Those are procedures that manipulate other procedures.
There's a similar story for data that I'm not going to go through. But the idea, again, is that we start with little pieces, we find ways of putting them together, and we find ways of reusing those ways of putting things together.
All right. What we're going to focus on now for the rest of the lecture, for software lab, and for design lab this week is thinking of a particular class of programs. So you've been working on writing programs that compute a value, typically. They take some input, they do some computation, and they generate an output, and then they're done
So that's one style of program, and that was the way people first conceptualized computer programs, because they had this big giant computer the size of this room or something. And they would feed some input into it, and then it would churn for a while, and then output would come out. And that was the model, right?
So computation is, well, some input comes in, you compute for a while, output comes out. But another thing, a really critical thing, that we do with computation is use it to control other systems. We might have a computational system that manages the heating vents and the lighting levels in this building in order to try to optimize energy and keep everybody at the right temperature and so on.
So in your car, or in your house, or in your iPod, or all kinds of places, there are computer systems that are connected with some external world, and that connection goes on and on and on. It's not just going to stop at some point and we get the answer. The view is that we're just going to have this long temporal interaction between some controller and some world outside.
So the kinds of abstractions that we typically use to define software systems don't necessarily work so well when what we're trying to do is control processes like this. So we need a different kind of abstraction. And the abstraction that we'll use for building controllers-- and eventually, also for understanding what happens when you connect a controller up with a world that it's trying to control-- is state machines.
So the idea of a state machine-- we'll draw them as these lovely blue boxes. Eventually they'll have more structure. A state machine is some kind of a box, and it gets inputs. In fact, it's going to get a stream of inputs at discrete time steps. It does some stuff inside, including maybe remembering something. And it generates an output.
So in every time step, we have this box. And every time step we feed an input, and it spits out an output. We feed it another input. It spits out another output.
So that's a state machine. It's some kind of box. There's something inside it. And what makes it a state machine and not a procedure or a function is that it's actually allowed to remember some stuff about what it saw before. Right?
The description I just gave you of a box where an input comes in and an output comes out-- actually, my third-grader had homework that looked like this. These little machines, a number comes in, a number comes out, and you say, oh that's the times-2 machine. OK. So that's a degenerate kind of a state machine. It's a simple kind of a state machine that doesn't have any memory.
But we're allowed-- in our state machines, the output doesn't have to depend only on the most recent input. The output can depend on the history of inputs that this machine has gotten. So the machine's allowed to remember something about what it's seen before and use that to compute the next output.
So that's what makes the difference between a state machine and something that's like a pure function. Does that make sense? Does anybody have any questions about that? Yeah.
STUDENT: Do you have an example of the times-2 box?
PROFESSOR LESLIE KAELBLING: So an example of a state machine that's like the times-2 box? Absolutely. I'll do it in just like one minute on the board. Yeah. Good.
So-- well, OK, no. Let's do it now. Somebody asked me for something. I can't resist.
So here's my favorite state machine. It's called an accumulator. And so numbers are going to come in and numbers are going to come out. And what this guy is going to do is remember the sum of the inputs it's seen so far.
So when you have a state machine, a really important idea is an initial state. The state-- this machine, it wakes up, and it's got some memory in it. It's got a place to remember something. So something has to be in there to start with. So state machines always need to have an initial state.
So let's say our accumulator wakes up, and its initial state is 0. OK. And then what my particular example is going to do is when it gets an input-- so it's going to get an input. Let's say it gets an input 1. Then its new state is going to be 1 and its output is going to be 1.
Now let's say it gets another input of 1. So new input comes in-- 1. My accumulator is going to take the input and the old state and add them together and get 2. And now the output's going to be 2.
OK. So that was an illustration of how this is not a function, right? Because the first time, we put in a 1, and what came out? 1. Second time we put in 1, what came out? 2.
OK. So what came out depended on the history. So my favorite simple state machine example is an accumulator. And it's not a function. That was great.
Another excellent question? Even a mediocre one? No? OK, I shouldn't say that to you. No? OK. I'll keep going. OK, right. So I think that's what we said.
So here's another example. I ride the T a lot, so I think about turnstiles. The turnstiles in the T are very complicated, though. They have whole computers, probably in the individual turnstile, and three kinds of cards, and they tell you how much money you have left, and so on. But let's think about a really old-school turnstile.
So it's a turnstile, and what you can do with this turnstile, we're going to think of it as a state machine. So you're going to give it some inputs. So here's the turnstile. Turnstile. Stile? I always want to put a Y in it. But no. OK.
So what are the possible inputs to the turnstile? What are the possible things we could do to this turnstile? Well, this turnstile I have, what you can do is you can put a coin it or you can try to push through. Or you can just stand and look at.
So those are my choices. So we could put in a coin. We can try to turn it. It might or might not turn. And we can just not do anything. That's what we can do at our turnstile.
And it's kind of a cool turnstile, because it has a sign up on the top. It's really the output. And the output, it can either say, Pay. So it says Pay when it really wants you to put a coin in. Or it can say-- what did I say? Enter.
So the sign on the top says either Pay or Enter. And that's the output of this turnstile. And the turnstile has a state, and the state of the turnstile is that it's either locked or not locked.
OK? Does that make sense? Turnstile?
OK. So here, on this slide, we have a description of the way the turnstile works. And to describe how a state machine works mathematically, we need to provide two functions and an initial state.
So here's the first of the two functions we have to provide. We have to give it the old state. We say, OK, if my state machine that I'm trying to describe, if it was previously in state s, and it just got an input i, what should its next state be?
So this is sometimes called a transition function or a next-state function. So we're going to say, well, OK, if the input is a coin, then my next state for this turnstile is going to be unlocked. Right? When I put a coin in, I become unlocked. I don't really care what state I was in before. If you put in a coin, now I'm unlocked.
If, on the other hand, the input is to turn the turnstile, either it's locked and I can't go through, or it was not locked and I can go through. But now it should become locked, because we don't want to let 10 more people in for free. That would be bad business, as a turnstile. So if the last input was turn, no matter what the previous state was, now the new state should be locked.
And otherwise, it should just stay in the same state. So if I'm just looking at the turnstile, eh. It's just going to not do anything. You can imagine a fancier turnstile that says, if it's been 10 minutes since somebody put a coin in, I should just lock myself again. But not this one. This is a simple one.
OK. Does this transition function make sense to everybody? It just says if I was in this state before and this was my input, what should the next state be? All right.
The other function that we have to specify is the output function. It takes also an old state and an input and it tells us what the output should be that goes with it. So here we have a simple output function. It says if my next state is going to be unlocked, I'm turning the sign on as soon as I can.
It doesn't exactly have to work this way, but the way I designed this particular machine is, if I know that I'm going to be unlocked on the next state, I'm going to turn on the Enter sign. And if I'm not going to be unlocked in the next state, then I'm going to turn on the Pay sign.
And then the last thing I have to specify is the initial state of the machine. So that is a mathematical description of a turnstile as a state machine. That's a state machine. It's got inputs, outputs, and states.
You should always know-- if you're doing something with a state machine, you should always know what the domain of inputs of the machine is, what's the domain of outputs, what's the domain of states What can they be? And then you have to know these functions that describe how it behaves, and you have to know an initial state.
When your state machine has a finite set of states, and smallish, it can be useful to think about it by drawing a state-transition diagram. And we're going to ask you to draw a state-transition diagram in design lab this week, so it's good to know what this is and means. So in a state-transition diagram, it's a picture of that same functional description we had on the previous page. So this diagram on this slide doesn't contain any more or less information than those descriptions of the transition and output functions and the initial state. It's just a different way of specifying it.
So here we have two nodes. So there's two circles, two blobs on our diagram. They're not circles. We've got ellipses here. And they stand for the states.
So we're going to have a node in this diagram, one for each state. The turnstile has two states, so we have these two nodes-- locked and unlocked. And now we draw arcs between the nodes to indicate the kinds of transitions we can make between the states and under what conditions we can make those transitions, and also what output will happen when we make the transitions. So those arcs are really loaded.
So let's look at this arc. It's easy to think about. Imagine the turnstile is currently locked. This arc says if we're in the locked state and the input is coin, then we should go to the unlocked state, and our output should be Enter.
So each of these arcs is labeled with two symbols. The first symbol is an inputs symbol and the slash, and then an output symbol. And it says that if you're in this state and you get this as an input, then you should generate that as an output and go to this state next. OK? So if it's locked, somebody puts in a coin, we say Enter and we go to the unlocked state.
If it's locked and somebody tries to push through, they try to do the turn action, then we say nuh-uh. It stays locked and we say Pay. So they can push as long as they want to, as many times as they want to. We just keep staying in the locked state.
Or they can stand and look at turnstile. That's cool, too. And so finally, they put in a coin, and then we go over to unlocked. And then it's the same sort of story over here.
So the transition function and the output function are encoded in these arcs and the labels. And this little stubby arrow here with the label Start just says which of the states we're starting in. OK. Is that good? Does everybody see how this diagram corresponds to that formal definition? Because we're going to be asking you to make diagrams like this later.
So let's see what happens. Imagine that we have a turnstile and we have a stream of inputs. I'm actually going to do this on the board. So imagine that the state starts out locked, in state 0, and we get an input of none.
OK. It's locked. We get an input of none, what's the output?
STUDENTS: Pay.
PROFESSOR LESLIE KAELBLING: Pay. OK. Now we get an input of coin. So we used locked and none together to decide that the output was Pay. We also used locked and none together to decide the new state. What's the new state?
STUDENTS: Locked.
PROFESSOR LESLIE KAELBLING: Locked. OK, good. Now, locked and we get an input, coin. What's the new state? Let's do it that way. What's the new state? Unlocked. And the new output?
STUDENTS: Enter.
PROFESSOR LESLIE KAELBLING: Enter. OK, good. You got it. So no matter how complicated a state machine is, if you know the transition diagram or you know the formal definition of the transition function and the output function, you can simulate that state machine.
You can say, if somebody gave me this sequence of inputs, what would the sequence of outputs be? Or what would the sequence of states be? So that's all you need to know, and you can tell me everything that you could want to know about that state machine. Oh yeah, I did give you the answer. Yeah. Good. OK.
So the reason that we like to think about state machines is that they help us encode the idea, for instance, that although my machine may have internal state that changes over time-- right? Your bank account has a balance that changes over time. The underlying rules of how my machine works don't change over time.
So mostly, the bank account plays by the same rules. Although some banks seem to change the rules rather frequently and in very small print. But usually the rules of the system stay the same, and some internal state is all that changes.
The turnstile, the rules of the turnstile are included in the transition diagram. So that means we can compactly describe how this thing is going to work forever. Right? We just said not just how do the first two steps of the turnstile work? We said, how's this turnstile going to work forever? And for any input sequence.
So very compactly, we were able to describe the infinite possible progression of the state of the system. So that's really important. We don't have to worry about looping and so on. And we'll see, later in this lecture, that it's a compositional system in the sense that we're going to be able to define state machines and put them together to make more complicated systems.
So at the end of the class, we're going to make a system where the robot actually has to drive around in an unknown map, build a map of the environment, decide where to drive, and so on. And we're going to do that by constructing several, now really quite complicated state machines. But we're going to think about the overall architecture of that system as being several state machines that are wired together, so that the outputs of some machines become the inputs of other machines.
And that's the way that we're going to do composition. We can take several state machines, glue them together, call it a new machine. Just like a procedure, now we can use that as a machine in a more complicated composition of things. So this is the sort of thing that we're going to be building up to, with this structure.
OK. So because in the end, we're going to implement state machines in Python and use that as a kind of state machine to study, we have to actually understand the classes and the types and the subclasses and the instances and all that. So I'm going to also take this opportunity to try to make quite clear some details of how inheritance works in Python's object-oriented programming system. Because that's going to be important to us in understanding how we build and manipulate state machines inside Python. So here's a kind of an overview, and then we'll look at each of these pieces in more detail.
OK. So we're going to define a class called SM for state machine. State machine class. It has, actually, several methods. There are three that we'll care about. Probably, in the end, you'll only end up calling transduce. But it's important to understand what all these methods do.
So the state machine class, it's a funny kind of class. It's what's sometimes called a generic or an abstract class. You can't say, please make me a state machine, right? Because you have to say what kind of state machine, right?
I can't say, just please make me a state machine and then run it for 10 steps and tell me what the answer is. You could say, make me a state machine that's an accumulator, or make me a state machine that plays by the rules of a turnstile. But you can't just make a totally generic state machine.
So we use this class in Python to store a bunch of procedures that we'll use that are important for state machines in general. But you can never make one of those. You can make it in Python, but it won't be useful. OK.
The next thing that we'll have is several classes for types of state machines. So we'll define a class for the class of turnstiles, or a class for the class of accumulators. And inside these classes-- so these classes will be a subclass. Turnstile is a subclass of SM. So it's going to inherit all the things that SM has in it.
And its job, the job of any of these classes for a class of state machines is to define two things. There are two critical things it has to define, or everything will break. It has to say what its start state is. We know that's part of the specification of a state machine. And we're also going to ask you to define, instead of two functions, really just one that does both jobs at once, because it lets us simplify some things computationally sometimes.
So there's going to be one function that you have to describe called getNextValues. It takes an old state of the state machine and an input, and it gives out the new state and the output. So actually, we can see it over here, maybe, better. So if the old state was locked and the input was none, and we fed those into a getNextValues procedure for the turnstile, locked and none, then we should get out a new state-- in this case, the new state will be locked-- and an output, which is Pay.
So that getNextValues takes in locked and none and it gives out unlocked and Pay. So it is just combined together those two functions that we need to describe the way a state machine works. I see a little bit of consternation in the back. Does somebody want to ask a question? I wait uncomfortably long.
No? OK. OK. Well, you can ask later, or ask on Piazza or something. All right.
So we have the state machine class that's generic. We have a turnstile class which describes the way all turnstile work. And then we can make a particular instance of a turnstile, Turnstile 452.
That is going to be an instance of the turnstile class. It will inherit all the methods and attributes that are inside the turnstile class. And then ultimately, it can have some state of its own.
Now I'm going to say this now, and I'm going to say it several times, and it won't necessarily sink in, and the tutor will say it to you a few more times, and then your TAs will say it to you some more times after that. But your job, when you define a class of state machines, is to define the start state and the getNextValues function. And the getNextValues function needs to be a function.
It doesn't need to change any internal variables inside the machine. So I'll show you this in environment diagrams in a minute. But it's really important to have the idea that this getNextValues is just a function. It's a function whose outputs depend only on its inputs. OK.
So let's look at the state machine class. State machine class, the important part of it is just this long. So that's not too much code, not too hairy.
Now remember that there's never an instance of this class all by itself. All the instances that we'll play with are instances of subclasses of this class. But this is the generic stuff that's true for all state machines. All state machines have these three methods, at the top.
There's a start method, which says, hey, state machine, start yourself up. And it says, well, OK, I'll start myself up. I'm going to set my state right now to be the initial state. So any state machine, you could say, hey, state machine, reset yourself. Go back to the initial state. And you do that by calling the start method on it. So start just puts a machine in its initial state.
The step method takes an input. So here's a state machine. It's gone along. It has some internal state. Maybe it's my accumulator.
Imagine it's my accumulator. My accumulator currently has 2 as its state. And I say, hey, accumulator, stop with the input 3.
What that's going to do is it's going to ask that particular machine, hey, machine, you're a special kind of machine. I don't really understand you, but you're able to tell me, if I tell you your current state and the input, you should be able to tell me what you want your next state to be and what the output should be. So I don't know exactly how you work, but I can say, hey, tell me your next values. And you do that.
And then I set your state to be that state, s. You don't set your state. I get to. The state machine class, up on the top, is the marionette. It's moving the strings. It's setting the states.
You're not doing it inside your class. State machine is doing that. And we return the output. So those are the important ways that we kind of make a state machine do something.
Transduce is a helper function that we'll use a lot. It takes a whole list of inputs, and it resets the state machine and then it feeds those inputs into the state machine one by one, just as we were doing here. And it gives out a list of outputs. So if we gave the turnstile state machine the list of inputs none, coin, transduce would give back to us the list of outputs Pay, Enter. If we gave it 10 inputs, then it would give us back 10 outputs, which would be the appropriate string of outputs for that string of inputs. OK. Yeah?
STUDENT: So the memory of the state machine is the state itself?
PROFESSOR LESLIE KAELBLING: The memory of the state machine is the state. That's exactly right.
STUDENT: OK. And would it make sense for us to add additional types of memory to the state machine other than the state? Or is that [INAUDIBLE]?
PROFESSOR LESLIE KAELBLING: Good. So the state of a state machine can be as complicated an object as you want it to be. It could be an array or a list or anything. But that's the state. So you want to think of it as the state, but the state can be complicated. Yeah. Good question. Other questions? Yep?
STUDENT: So with the class getNextValues, it has the variables self, state, and input. What is the self [INAUDIBLE]?
PROFESSOR LESLIE KAELBLING: Excellent. I'm I going to have a beautifully detailed example of that in just a minute. You're perfect. You just asked me the next question I need. In two slides, I get there.
OK. So that was the code for the state machine class, right? For all state machines. Now if we want to describe a turnstile, here's how we describe it. It has a start state and a getNextValues.
I'm going to just go past that because I like the accumulator better. So we're going to talk about that. It's more compact. You should study the turnstile example, too, but we have limited time and patience.
So Accumulator is pretty easy to describe-- at least the one that I described here. It has a start state of 0. And there's the getNextValues procedure. It has self, state, and input as arguments. And it's going to return state plus input, because we decided that's what that the next state was going to be. And it's going to return state plus input again, because we decided that's what the output was going to be.
It doesn't depend on self. And in fact, it shouldn't. So I'll show you in a diagram what role self is playing here.
But for our getNextValues methods, they're really just pure functions. They should really depend only on the old state and the input. So the new state and the output should depend only on the old state and the input as passed in to getNextValues. So getNextValues doesn't need to look at itself.
We're really using these classes, these subclasses of SM, just as a place to store the appropriate getNextValues procedure. But they don't have-- we're not going to directly mess with their state. We're going to trust the state machine machinery to mess with the state directly. So this just computes the next one.
Think of it as a hypothetical question. One of the reasons that we do it this way is because at the end of the term, we'll be building systems that do planning. They consider different sequences of actions. If I were to go forward, forward, forward, and then left, would that be better or worse than going forward, left, forward, left?
So our program wants to consider that question. And so it has to ask a lot of hypothetical questions in its head. Well, if I were to do this, what would happen?
You can think of getNextValues as being a sort of hypothetical question. If you were in this state and you got this input, what would you do? I'm not telling you, right now, you're in that state. I'm just saying, if you were in that state and you got this input, what would you do?
Think of getNextValues as being that, as answering hypothetical questions. If you were here and you did this, what would happen?
OK. So here is a problem to think about. And I think that I would like you to actually spend a little time thinking about it, and then we'll go through a description of it in a fair amount of detail afterwards. So just take a couple minutes and see if you can think about what the values here are and what the answer would be.
You can talk to people next to you if you want to. I should have told you to do that.
OK. Let's answer the first question. Should be easier. So what a.state?
How many people vote for 5? How many people vote for 15? How many people vote for none of the above? OK, so the 5s have it.
And the 5 is right. You can sort of see that intuitively. Because let's see. We start a. What's a's initial state?
STUDENTS: 0.
PROFESSOR LESLIE KAELBLING: 0. Right, because that's the initial state of the Accumulator class we defined. We step a with an input of 7. That means its new state is 7.
And then we mess around with b, and we'll draw a picture so we really understand what's going on. We mess around with b, but that doesn't change a. And then we step a with minus 2. That adds minus 2 to its current state, which is 7, and we get a state of 5. So that's why it's state 5.
Here's another question. Are a and b in different states at the end of this transcript? A and b are in different states. Will a.getNextValues of 8, 13 ever be different from b.getNextValues of 8, 13?
STUDENTS: No.
PROFESSOR LESLIE KAELBLING: No. Good. That's right. They're different instances of the same subclass of state machines. And that subclass of state machines has the getNextValues procedure stored in it.
And that procedure is a pure function. Its value depends only on the arguments. So it doesn't depend on the current state of a or the current state of b. The output of getNextValues for this class of machines will always only depend on the arguments that we pass in. So that's a really important point.
Now what I want to do is actually belabor this point some. Because here, it's not too hard to intuit the answer. But as these things get more complicated, it'll get harder and harder to intuit.
And lots of people want to rely on intuition for understanding complicated computer programs or complicated things like these state machines. And I find that even though I've been doing this for a long time, intuition takes me pretty far, but eventually, I get to really kind of hairy, confused cases with lots of nested procedures, or too many subclasses, or I don't know. And then it's not easy to just look at the text of the program and know what it means.
So we have to fall back on the definitions. So it's like, well, sometimes maybe you could look at an arithmetic expression and know what it means, but sometimes you have to actually kind of go through and understand, piece by piece, what the pieces mean and how they go together. And that's what the Python interpreter does.
And what I want to do-- I spend some time with you in the little mini-lectures and in the readings on understanding programs in terms of environment diagrams. But I want to understand this set of programs in terms of environment diagrams. And I think if you get good at the tool of understanding, of being able to build environment diagrams, then you can understand absolutely any program you ever run across. Because you can approach it the way the computer does. And the computer can understand any program that's legally specified.
So I'm going to give you the tools to make understanding programs not into an act of art or intelligence, but an act of mundane repetition of some simple rules. So just as you know that you can evaluate any arithmetic expression I gave you-- you might hate it, and it might take forever, and you might make some arithmetic errors along the way, because you drop signs when you multiply. But you know you could evaluate any arithmetic expression.
The same thing should be true for any Python program. You should know that you could compute the value of any Python program in exactly the same way. You just have to know the rules.
So this environment diagram story is a way of knowing the rules. OK. So that was probably more speech than you wanted.
So here, let's draw an environment diagram for this situation that we've been thinking about. So we have a class called State Machine. And a class is an environment. So a class is an environment.
And a state machine has a couple of useful things to find in it. I'm not going to worry about transduce. It has a procedure start and a procedure step. So those are the names that are defined inside the class SM. And these guys are procedures. I'm not going to draw the little procedure boxes in detail, but if you don't remember how that goes, you should read about it in the readings or watch the little lectures about the environment model.
So that's our State Machine class. And remember, that a class is an environment, and every environment has a pointer back to its parent environment. So an environment is something that maps from names to values, and it has a parent pointer.
OK. So there's the State Machine class. So when we defined the state machine class for you, this is what you got. Now you came along and defined the Accumulator class.
So you defined the Accumulator class. I'm going to call it Acc so it fits in here nicely. And when we define the Accumulator class, what we did, we made another environment, and it also has two entries in it. But the two entries, when we defined a new class of state machines, a new particular kind, we always have start state, which for our Accumulator is 0. And a getNextValues, which I am abbreviating for the sake of board space. That stands for getNextValues. And that's going to be a procedure, also. Some procedure.
And the relationship between a class and its subclass-- remember, classes and subclasses are environments. The relationship between them is that the parent pointer of the subclass environment goes up to the class environment. So it goes like that. OK. So that's the Accumulator.
Now when we make an instance of a-- we're going to make two instances. We'll just-- I'll do them both here at the same time. So here's a and b. They're going to be instances of the Accumulator class. So they're going to be environments, little environments of their very own. Here's one. Here's another one. And they're instances of the class Accumulator, so their parent pointers go here. OK.
So at the moment that I have created two Accumulators but not called start or step, this is effectively what the memory of the computer looks like-- obviously, at some level of abstraction. Do you have questions about this picture? Surely some of you do. I don't believe that nobody does. Yeah?
STUDENT: Is there significance to where in your boxes the arrow's pointing?
PROFESSOR LESLIE KAELBLING: No, they're pointing to the environment as a whole. Right. Right. So they're just pointing to a big box. Other questions? OK.
So let's just simulate two of these calls that are being made here, and see what happens to our memory picture when we make them. OK? So let's start with a.start. So imagine we're going to do a.start.
OK. Here's a trick question. What's the value of a.start? It's a method. Right? A.start? If you type into Python a.start, it will say bound method, blah, blah, blah. It's the name of a procedure.
How do we get that? Well a-- what does a.start mean? First we evaluate a out [? in ?] our environment. We evaluate a. And a is this environment here. So this is a.
Dot means, please look this name up in the environment for me. So we look start up in this environment. Do we see start in here? No.
If we don't see start in an environment, we follow its parent pointer and we look in here for start. Do we see start? No.
That's OK. We follow its parents pointer. We look up here. We look for start. We say, yay, we found start. And we return its value.
So there's this procedure right here-- that procedure is the value of the expression a.start. It's not a mystery. It's not that Python doesn't like you. It's not that you did something wrong-- unless that's not what you meant to type. It's a perfectly reasonable thing to type. But it's the name of a method. OK.
Now what if we wanted to call that method? If we wanted to call that method, we would write a.start like that. And that says, please call the method that we find by evaluating this argument-- please call it with the argument a, or the argument that is the object that we get by evaluating a, and whatever other objects are in the argument list. There aren't any other ones there.
So someone asked me about self. Here's where self is going to start to happen. OK. So the procedure of interest is the top one, which is the start procedure. Let me show you the start procedure. Start procedure is here, here on the text. Self.state equals self.startState.
OK. That's a weird thing to write down. Intuitively, it might not make sense to you. But if we're just, like, stupid and literal-minded like a computer, we can understand it. So let's do it.
So we're going to evaluate-- so we're going to deal with calling that start method. So what happens when you call a method is that you make a binding environment. Always, always, when you call a method, you make a binding environment.
And you bind the arguments of the method to the values that they're going to have. So this method has one argument, self. Note-- if I took that piece of code and I replaced the word self with the word orangutan, it would work exactly the same. That's really important.
Self is not a magic word in Python. We use it to mean the objects that we're applying this method to, but it's always the first argument in a method. But it doesn't matter what it's named. OK.
So it happens to be named self here, so I happen to make a mining environment with self. If it was named orangutan, I would make a binding environment with orangutan. It would be fine. OK.
What goes there is the value that we get by evaluating a. The value we get by evaluating a is this guy. So it's going to be a long pointer, but hey. OK, good. So that's what self is, that object a.
So now we've made this binding environment, and now we're ready to evaluate the body of that procedure inside this environment. So let's do that. Oh, the environment needs a parent. The parent-- oh, the parent-- the parent is all the way over here. It's not going to matter, but it does need one, just for completeness. OK.
So now we're going to evaluate self.state equals self.startState. OK. So it's an assignment statement. Let's start by evaluating the stuff on the right-hand side of the equality.
First, we evaluate self. In this environment, that gives us this object. Now we do self.startState. So dot means please look up the word startState in this environment.
We look up startState in this environment, we don't find it. We look it up here, and we do find it. Yay. so startState is 0. So the value of self.startState is 0.
Is that so far so good? Do you see why startState was 0? I started in the instance. Self is the instance. And I looked up until I found a value for it, and I found it in the subclass. OK.
Now I have this assignment statement that says, self.state should get 0. So I go and look at my self-- that's here-- and I say, do I have an attribute named state? No. Well, that's OK. I'll make one. So this is the moment where the state attribute gets made, and then it gets assigned the value 0.
So that was the execution of the start method on a. When we did a.start, that's how it happened. Questions about that? I'm going to do one more of these, because I really want you to just do it without thinking. And it feels weird at first, but eventually, it's easy.
OK. So that was a.start. So we're just going to do a step.
So once we're done, once Python's done evaluating a.start, it doesn't need this binding environment anymore, so we can erase it. I'm going to erase it. Sorry for the squeakiness. We'll leave that pointer. It's going to be useful again in a minute. OK.
So now let's do a.step. Was it 10? I don't know. Was it 10?
STUDENT: It was 7.
PROFESSOR LESLIE KAELBLING: 7. OK, good. Thank you. A.step of 7. OK.
So similar story. We'll do it in slightly less detail. We look up a. That's this environment. We look up a over here, we get this guy.
A.step takes us to this procedure. We look at that procedure. We see that it has two arguments. So we make an environment with two arguments. The two arguments are self and inp.
Self is this guy. So I hook back up to that old pointer I had. And inp is going to be 7. And this guy's parents goes there. So far so good? OK.
Now I'm going to evaluate the text of the step procedure in this environment. For that, I will need a new board. Yeah. This'll work. OK, good.
So I'm going to evaluate the text of that procedure in this environment. So let's take the first statement-- after this, it's easy. S, O is equal to getNextValues of self.state and inp. OK.
So I'm going to evaluate this expression in that environment. So self.state-- notice that this isn't a parent pointer thing. I don't find state by looking up in my environments. I find state by saying, I have this object, self, in my hand, and I'm going to go look up the name state in the object self. And the object self has state right there. So self.state is 0. And inp is seven. OK.
So now what do I do? I have to evaluate getNextValues. Oops! OK, oh, you should have yelled at me. Why should you have yelled at me? I did the thing that we all do all the time, all the time, all the time. I forgot--
STUDENT: Self.
PROFESSOR LESLIE KAELBLING: Self. getNextValues is not defined in that environment. Does this environment have getNextValues in it? No. Does its parent environment have getNextValues in it? No.
So when I say getNextValues, Python says, pbbbt, undefined. If it would make sound effects, it would be better, but no. OK.
So I had to have self. So. Yell at me when I do stuff like that. OK.
Self.getNextValues. So what is self.getNextValues? Well, self, self is still self. It's the same self. So self is this guy.
So we look at this guy's getNextValues. It's this getNextValues. It's this procedure. It's the getNextValues for Accumulator. Right?
So, OK. So now we know that this is the getNextValues procedure from the Accumulator. So the getNextValues procedure from the Accumulator is there. This is good. I got a couple more minutes, so I'm going to keep going.
getNextValues for the Accumulator, it's a procedure with three arguments. When it's time to apply a procedure, I make a binding environment with three things in it. Self, state, inp.
Self is going to be that guy. State is 0. Inp is 7. What do we return? 7, 7. Yeah?
We return 7, 7. So S, O equals 7, 7. That's what this statement says. Where did the S and the O go? Where do I write that down in the memory of the computer?
STUDENT: Binding environment.
PROFESSOR LESLIE KAELBLING: In the binding environment. Good, good, good, good, good. Right here. Right here. S and O are 7 and 7. OK.
So where we are is we're back here, executing the step procedure. So we just evaluated this thing. We got back 7 and 7. We assign S to be 7 and O to be 7. Those are little local variables in the environment that we made for this procedure.
Now we say, self.state equals S. What's S? 7. Where's self.state? It's here, right? Boy, that was the microphone. It's not going to be happy. 7.
Good. Self.state 7. And we return 7. Yay. So the return value for that a.step of 7 there is 7.
So, OK. So lots of complicated stuff happens behind the scenes, but none of it is hard. It's just complicated. So you can do it if you need to. If you're ever confused about what a piece of code does, you can do that.
That was the answer. We knew that. OK.
So what I've said about state machines-- we've spent a lot of time now studying how to define a particular kind of state machine. It's like a Turnstile or an Accumulator. And the next thing that we need to do-- which we're not going to do in great detail now, but it's described in the readings and in some mini-lectures-- is see how we can take state machines and put them together.
So for instance-- so we have two, or actually, really, three sort of ways of putting machines together. We have the cascade method, which takes two state machines and it just hooks them up so that the output of one machine is the input of the next one. We also have a way of running machines in parallel, of taking the input and feeding that input into one machine, feeding that same input into the other machine, and getting their outputs, a tuple of their outputs as the output of the resulting machine.
We'll also see, and we'll spend a lot of time thinking about feedback. We'll be able to do things like take a machine and take its output, and just run it around and plug it in as the input. So that can cause some really interesting and cool behavior. Feedback.
What's crucial about these things is that once I have taken these machines and combined them in one of these ways, what I have in my hands again is a state machine. And that state machine can be further combined with other state machines in these same kinds of ways. So you can make a feedback of a parallel and a cascade, and then feedback into something else, and so on. So you can arbitrarily put things together into more and more complicated systems.
I'll ask you one question here, and then we'll be done. So imagine that I make two Accumulators now, same old Accumulators that we had before, and I hook them up in a cascade. So what I'm going to do is I'm going to take one Accumulator, I'm going to take its output and feed it into the next Accumulator. OK? Now that's a new machine.
And I could say, well, what happens if I put the sequence 7, 3, 4 into that combined machine? I'm just going to show you the answer. We'll talk about it.
So if we just ignore b here for a minute, we know that if we put 7, 3, 4 into an Accumulator, we'll get 7, 10, 14, right? Because those are just each time, the sums of all of the previous inputs. And if you take that sequence and feed it into be, you'll get 7, 17, 31.
Now when we operate these machines, we don't necessarily feed the whole sequence into a and then take that whole sequence and feed it into b. We can do it incrementally. And you'll have a tutor problem on actually how to make Python do that. But it's important, again, to know that the putting the machines together is not magic either. OK.
This week, software lab, design lab, homework. Homework! Wait, shush. Hang on.
There's a homework assignment going out on Wednesday. It's due in two weeks. It's kind of hard. Don't leave it until the night before.
Also, the next lecture is Tuesday, 32-123. No software lab next week. Design lab next week.