[go: up one dir, main page]

0% found this document useful (0 votes)
147 views156 pages

6.01 Course Notes Spring 2011

This document provides an overview and goals for the 6.01 course. The primary goal is for students to learn and apply the principles of modularity and abstraction in various engineering contexts involving systems that interact with and control an external environment. A second goal is to practice making mathematical models of real systems that provide insights into problem solving. Students will work individually and in pairs on open-ended problems requiring explanation and justification of approaches rather than single right answers, exposing them to ubiquitous engineering trade-offs. The course will also aim to engage students more actively in the learning process.

Uploaded by

诸葛亮
Copyright
© Attribution Non-Commercial (BY-NC)
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
147 views156 pages

6.01 Course Notes Spring 2011

This document provides an overview and goals for the 6.01 course. The primary goal is for students to learn and apply the principles of modularity and abstraction in various engineering contexts involving systems that interact with and control an external environment. A second goal is to practice making mathematical models of real systems that provide insights into problem solving. Students will work individually and in pairs on open-ended problems requiring explanation and justification of approaches rather than single right answers, exposing them to ubiquitous engineering trade-offs. The course will also aim to engage students more actively in the learning process.

Uploaded by

诸葛亮
Copyright
© Attribution Non-Commercial (BY-NC)
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 156

Chapter 6.

01— Spring 2011— February 1, 2011 1

6.01 Course Notes


Spring 2011

1 Course Overview 4
1.1 Goals for 6.01 4
1.2 Modularity, abstraction, and modeling 4
1.2.1 Example problem 5
1.2.2 An abstraction hierarchy of mechanisms 5
1.2.3 Models 10
1.3 Programming embedded systems 12
1.3.1 Interacting with the environment 12
1.3.2 Programming models 14
1.4 Summary 16
2 Learning to Program in Python 17
2.1 Using Python 17
2.1.1 Indentation and line breaks 18
2.1.2 Types and declarations 19
2.1.3 Modules 20
2.1.4 Interaction and Debugging 20
2.2 Procedures 21
2.3 Control structures 23
2.3.1 Conditionals 23
2.4 Common Errors and Messages 30
2.5 Python Style 32
2.5.1 Normalize a vector 32
2.5.2 Perimeter of a polygon 33
2.5.3 Bank transfer 34
2.5.4 Coding examples 35
Chapter 6.01— Spring 2011— February 1, 2011 2

3 Programs and Data 41


3.1 Primitives, Composition, Abstraction, and Patterns 42
3.2 Expressions and assignment 43
3.2.1 Simple expressions 43
3.2.2 Variables 44
3.3 Structured data 46
3.3.1 List mutation and shared structure 48
3.3.2 Tuples and strings 52
3.4 Procedures 54
3.4.1 Definition 55
3.4.2 Procedure calls 56
3.4.3 Non-local references 59
3.4.4 Environments in Python 60
3.4.5 Non-local references in procedures 61
3.4.6 Procedures as first-class objects 62
3.5 Object-oriented programming 67
3.5.1 Classes and instances 68
3.5.2 Methods 71
3.5.3 Initialization 74
3.5.4 Inheritance 76
3.6 Recursion 79
3.7 Implementing an interpreter 81
3.7.1 Spy 81
3.7.2 Evaluating Spy expressions 83
3.7.3 Object-Oriented Spy 91
3.8 Object-Oriented Programming Examples 96
3.8.1 A simple method example 96
3.8.2 Superclasses and inheritance 97
3.8.3 A data type for times 99
3.8.4 Practice problem: argmax 100
3.8.5 Practice problem: OOP 101
3.8.6 Practice problem: The Best and the Brightest 102
3.8.7 Practice Problem: A Library with Class 104
Chapter 6.01— Spring 2011— February 1, 2011 3

4 State Machines 107


4.1 Primitive state machines 108
4.1.1 Examples 109
4.1.2 State machines in Python 113
4.1.3 Simple parking gate controller 122
4.2 Basic combination and abstraction of state machines 124
4.2.1 Cascade composition 124
4.2.2 Parallel composition 126
4.2.3 Feedback composition 127
4.2.4 Plants and controllers 135
4.2.5 Conditionals 138
4.2.6 Switch 138
4.3 Terminating state machines and sequential compositions 140
4.3.1 Repeat 141
4.3.2 Sequence 143
4.3.3 RepeatUntil and Until 145
4.4 Using a state machine to control the robot 147
4.4.1 Rotate 148
4.4.2 Forward 149
4.4.3 Square Spiral 150
4.5 Conclusion 153
4.6 Examples 153
4.6.1 Practice problem: Things 153
4.6.2 Practice problem: Inheritance and State Machines 155
Chapter 1 Course Overview 6.01— Spring 2011— February 1, 2011 4

Chapter 1
Course Overview

1.1 Goals for 6.01


We have many goals for this course. Our primary goal is for you to learn to appreciate and use
the fundamental design principles of modularity and abstraction in a variety of contexts from
electrical engineering and computer science. To achieve this goal, we will study electrical engi-
neering (EE) and computer science (CS) largely from the perspective of how to build systems that
interact with, and attempt to control, an external environment. Such systems include everything
from low-level controllers like heat regulators or cardiac pacemakers, to medium-level systems
like automated navigation or virtual surgery, to high-level systems that provide more natural
human-computer interfaces.
Our second goal is to show you that making mathematical models of real systems can help in the
design and analysis of those systems; and to give you practice with the difficult step of deciding
which aspects of the real world are important to the problem being solved and how to model
them in ways that give insight into the problem.
We also hope to engage you more actively in the educational process. Most of the work of this
course will not be like typical problems from the end of a chapter. You will work individually
and in pairs to solve problems that are deeper and more open-ended. There will not be a unique
right answer. Argument, explanation, and justification of approach will be more important than
the answer. We hope to expose you to the ubiquity of trade-offs in engineering design: it is rare
that an approach will be best in every dimension; some will excel in one way, others in a different
way. Deciding how to make such trade-offs is a crucial part of engineering.
Another way in which we hope to engage you in the material is by having many of you return
to the course as lab assistants in future semesters. Having a large number of lab assistants in the
class means that students can be given more open-ended problems, and have people around to
help them when they are stuck. Even more importantly, the lab assistants are meant to question
the students as they go; to challenge their understanding and help them see and evaluate a variety
of approaches. This process is of great intellectual value to student and lab assistant alike.
Finally, of course, we have the more typical goals of teaching exciting and important basic ma-
terial from electrical engineering and computer science, including modern software engineering,
linear systems analysis, electronic circuits, and decision-making. This material all has an internal
elegance and beauty, as well as crucial role in building modern EE and CS systems.

1.2 Modularity, abstraction, and modeling


Whether proving a theorem by building up from lemmas to basic theorems to more specialized
results, or designing a circuit by building up from components to modules to complex processors,
Chapter 1 Course Overview 6.01— Spring 2011— February 1, 2011 5

or designing a software system by building up from generic procedures to classes to class libraries,
humans deal with complexity by exploiting the power of abstraction and modularity. Without
such tools, a single person would be overwhelmed by the complexity of a system, as there is only
so much detail that a single person can consciously manage at a time.
Modularity is the idea of building components that can be re-used; and abstraction is the idea
that after constructing a module (be it software or circuits or gears), most of the details of the
module construction can be ignored and a simpler description used for module interaction (the
module computes the square root, or doubles the voltage, or changes the direction of motion).
Given basic modules, one can move up a level of abstraction and construct a new module by
putting together several previously-built modules, thinking only of their abstract descriptions,
and not their implementations. And, of course, this process can be repeated over many stages.
This process gives one the ability to construct systems with complexity far beyond what would
be possible if it were necessary to understand each component in detail.
Any module can be described in a large number of ways. We might describe the circuitry in a
digital watch in terms of how it behaves as a clock and a stopwatch, or in terms of voltages and
currents within the circuit, or in terms of the heat produced at different parts of the circuitry. Each
of these is a different model of the watch. Different models will be appropriate for different tasks:
there is no single correct model. Rather, each model exposes different dimensions of the system,
allowing us to explore many aspects of the design space of a system, and to trade off different
factors in the performance of a system.
The primary theme of this course will be to learn about different methods for building modules
out of primitives, and of building different abstract models of them, so that we can analyze or
predict their behavior, and so we can recombine them into even more complex systems. The same
fundamental principles will apply to software, to control systems, and to circuits.

1.2.1 Example problem


Imagine that you need to make a robot that will roll up close to a light bulb and stop a fixed
distance from it. The first question is, how can we get electrical signals to relate to the physical
phenomena of light readings and robot wheel rotations? There is a large part of electrical engi-
neering related to the design of physical devices that connect to the physical world in such a way
that some electrical property of the device relates to a physical process in the world. For example,
a light-sensitive resistor (photo-resistor) is a sensor whose resistance changes depending on light
intensity impinging on it; a motor is an effector whose rotor speed is related to the voltage across
its two terminals. In this course, we will not examine the detailed physics of sensors and effectors,
but will concentrate on ways of designing systems that use sensors and effectors to perform both
simple and more complicated tasks. To get a robot to stop in front of a light bulb, the problem will
be to find a way to connect the photo-resistor to the motor, so that the robot will stop at an appro-
priate distance from the bulb. Thus, we will already use the idea of abstraction to treat sensors
and effectors as primitive modules whose internal details we can ignore, and whose performance
characteristics we can use as we design systems built on these elements.

1.2.2 An abstraction hierarchy of mechanisms


Given the light-sensitive resistor and the motor, we could find many ways of connecting them,
using bits of metal and ceramic of different kinds, or making some kind of magnetic or mechanical
linkages. The space of possible designs of machines is enormous.
Chapter 1 Course Overview 6.01— Spring 2011— February 1, 2011 6

One of the most important things that engineers do, when faced with a set of design problems,
is to standardize on a basis set of components to use to build their systems. There are many
reasons for standardizing on a basis set of components, mostly having to do with efficiency of
understanding and of manufacturing. It is important, as a designer, to develop a repertoire of
standard bits and pieces of designs that you understand well and can put together in various ways
to make more complex systems. If you use the same basis set of components as other designers,
you can learn valuable techniques from them, rather than having to re-invent the techniques
yourself. And other people will be able to readily understand and modify your designs.
We can often make a design job easier by limiting the space of possible designs, and by standard-
izing on:
• a basis set of primitive components;
• ways of combining the primitive components to make more complex systems;
• ways of “packaging” or abstracting pieces of a design so they can be reused (in essence creat-
ing new “primitives”); and
• ways of capturing common patterns of abstraction (essentially, abstracting our abstractions).
Very complicated design problems can become tractable using such a primitive-combination-
abstraction-pattern (PCAP) approach. In this class, we will examine and learn to use a variety
of PCAP strategies common in EE and CS, and will even design some of our own, for special pur-
poses. In the rest of this section, we will hint at some of the PCAP systems we will be developing
in much greater depth throughout the class. Figure 1.1 shows one view of this development, as a
successive set of restrictions of the design space of mechanisms.

electronic computers with


Python programs that
select outputs by
planning

electronic computers
running Python programs
electronic computers
digital electronic circuits

traditional analog circuits

electrical systems

Figure 1.1 Increasingly constrained systems.

One very important thing about abstract models is that once we have fixed the abstraction, it
will usually be possible to implement it using a variety of different underlying substrates. So,
as shown in figure 1.2, we can construct general-purpose computers out of a variety of different
kinds of systems, including digital circuits and general-purpose computers. And systems satisfy-
ing the digital circuit abstraction can be constructed from analog circuits, but also from gears or
water or light.
Chapter 1 Course Overview 6.01— Spring 2011— February 1, 2011 7

General-purpose computer

Turing Cellular
Digital circuits
machine automata

Analog
Gears Light Water
electronics

Figure 1.2 A single abstraction may have a variety of different underlying implementations.

Another demonstration of the value of abstracted models is that we can use them, by analogy, to
describe quite different systems. So, for example, the constraint models of circuits that we will
study can be applied to describing transmission of neural signals down the spinal cord, or of heat
through a network of connected bodies.
Let’s explore the abstraction hierarchy figure 1.1 in some more detail, moving up abstraction lev-
els while observing common patterns.

Circuits
Typical electronics circuits are built out of a basis set of primitive components such as voltage
sources, resistors, capacitors, inductors and transistors. This set of component types was chosen
to be closely related to primitive concepts in our physical understanding of electronic systems
in terms of voltage, current, resistance, and the rate of change of those quantities over time. So,
when we buy a physical resistor, we tend to think only of its resistance; and when we buy a
capacitor, we think of its ability to store charge. But, of course, that physical resistor has some
capacitance, and the capacitor, some resistance. Still, it helps us in our designs to have devices
that are as close to the ideal models as possible; and it helps that people have been designing with
these components for years, so that we can adopt the strategies that generations of clever people
have developed.
The method of combining circuit elements is to connect their terminals with conductors (wires,
crimps, solder, etc.), a process that is generically referred to as wiring. And our method of ab-
straction is to describe the constraints that a circuit element exerts on the currents and voltages of
terminals to which the element is connected.
So, armed with the standard basis set of analog circuit components, we can try to build a circuit to
control our robot. We have a resistance that varies with the light level, but we need a voltage that
does so, as well. We can achieve this by using a voltage divider, which is shown in figure 1.3A.
Using an abstract, constraint-based model of the behavior of circuit components that we will
study in detail later in the course, we can determine the following relationship between Vout and
Vin :
RB
Vout = Vin .
RA + RB
So, for example, if RA = RB , then Vout = Vin /2. Or, in our case, if RA actually varies with the
amount of light shining on it, then Vout will also vary with the light. 1
That is great. So, now, we might imagine that we could use this voltage difference that we have
created to drive the motor at different speeds, depending on the light hitting the resistor, using a
circuit something like the one shown in figure 1.3B. But, sadly, that will not work. It turns out that

1
Do not worry if this example does not make much sense to you; we will explore this all in great detail later.
Chapter 1 Course Overview 6.01— Spring 2011— February 1, 2011 8

Vin Vin Vin

RA RA RA

Vout Vout
Vout

RB RB motor RB motor

Figure 1.3 Voltage dividers: A. Resistor divider. B. Connected to a motor, in a way that breaks
the abstraction from part A. C. Connected to a motor, with a buffer.

once we connect the motor to the circuit, it actually changes the voltages, and we can no longer
maintain the voltage difference needed to drive the motor.
So, although we have developed an abstract model of the behavior of circuit components, which
lets us analyze the behavior of a particular complete circuit design, it does not give us modu-
larity. That is, we cannot design two parts of a circuit, understand each of their behaviors, and
then predict the behavior of the composite system based on the behavior of the separate compo-
nents. Instead, we would have to re-analyze the joint behavior of the whole composite system.
Lack of modularity makes it very difficult to design large systems, because two different people,
or the same person at two different times, cannot design pieces and put them together without
understanding the whole.
To solve this problem, we can augment our analog-circuit toolbox with some additional compo-
nents that allow us to design components with modular behavior; they “buffer” or “isolate” one
part of the circuit from another in ways that allow us to combine the parts more easily. In this
class, we will use op-amps to build buffers, which will let us solve our sample problem using a
slightly more complex circuit, as shown in figure 1.3C.
Thus, the key point is that good modules preserve abstraction barriers between the use of a mod-
ule and internal details of how they are constructed. We will see this theme recur as we discuss
different PCAP systems.

Digital circuits
In analog circuits, we think about voltages on terminals ranging over real values. This gives us
the freedom to create an enormous variety of circuits, but sometimes that freedom actually makes
the design process more difficult. To design ever more complex circuits, we can move to a much
stronger, digital abstraction, in which the voltages on the terminals are thought of as only taking
on values that are either “low” or “high” (often called 0 and 1). This PCAP system is made up of
a basis set of elements, called gates, that are built out of simpler analog circuit components, such
as transistors and resistors. Adopting the digital abstraction is a huge limitation on the kinds
of circuits that can be built. However, digital circuits can be easy to design and build and also
can be inexpensive because the basic elements (gates) are simple (fewer transisters are need to
construct a gate than to construct an op amp), versatile (only a small number of different kinds
of logic elements are necessary to construct an arbitrary digital circuit), and combinations are
easy to think about (using Boolean logic). These properties allow designers to build incredibly
Chapter 1 Course Overview 6.01— Spring 2011— February 1, 2011 9

complex machines by designing small parts and putting them together into increasingly larger
pieces. Digital watches, calculators, and computers are all built this way.
Digital design is a very important part of EECS, and it is treated in a number of our courses at basic
and advanced levels, but is not one of the topics we will go into in detail in 6.01. Nevertheless,
the same central points that we explore in this course apply to this domain as well.

Computers
One of the most important developments in the design of digital circuits is that of the general-
purpose “stored program” computer. Computers are a particular class of digital circuits that are
general purpose: the same actual circuit can perform (almost) any transformation between its
inputs and its outputs. Which particular transformation it performs is governed by a program,
which is some settings of physical switches, or information written on an external physical mem-
ory, such as cards or a tape, or information stored in some sort of internal memory.
The “almost” in the previous section refers to the actual memory capacity of the computer. Exactly
what computations a computer can perform depends on the amount of memory it has; and also
on the time you are willing to wait. So, although a general-purpose computer can do anything
a special-purpose digital circuit can do, in the information-processing sense, the computer might
be slower or use more power. However, using general-purpose computers can save an enormous
amount of engineering time and effort. It is much cheaper and easier to debug and modify and
manufacture software than hardware. The modularities and abstractions that software PCAP
systems give us are even more powerful than those derived from the digital circuit abstraction.
Again, we can see how abstraction separates use from details; we don’t need to know how the
circuits inside a computer are designed, we just need to know the rules by which we can use them
and the constraints under which they perform.

Python programs
Every general-purpose computer has a different detailed design, which means that the way its
program needs to be specified is different. Furthermore, the “machine languages” for describing
computer programs, at the most primitive level, are awkward for human programmers. So, we
have developed a number of computer programming languages for specifying a desired com-
putation. These languages are converted into instructions in the computer’s native machine lan-
guage by other computer programs, called compilers or interpreters. One of the coolest and most
powerful things about general-purpose computers is this ability to write computer programs that
process or create or modify other computer programs.
Computer programming languages are PCAP systems. They provide primitive operations, ways
of combining them, ways of abstracting over them, and ways of capturing common abstraction
patterns. We will spend a considerable amount of time in this course studying the particular
primitives, combination mechanisms, and abstraction mechanisms made available in the Python
programming language. But the choice of Python is relatively unimportant. Virtually all modern
programming languages supply a similar set of mechanisms, and the choice of programming
language is primarily a matter of taste and convenience.
In a computer program we have both data primitives and computation primitives. At the most
basic level, computers generally store binary digits (bits) in groups of 32 or 64, called words.
These words are data primitives, and they can be interpreted by our programs as representing
Chapter 1 Course Overview 6.01— Spring 2011— February 1, 2011 10

integers, floating-point numbers, strings, or addresses of other data in the computer’s memory.
The computational primitives supported by most computers include numerical operations such
as addition and multiplication, and locating and extracting the contents of a memory at a given
address. In Python, we will work at a level of abstraction that does not require us to think very
much about addresses of data, but it is useful to understand that level of description, as well.
Primitive data and computation elements can be combined and abstracted in a variety of differ-
ent ways, depending on choices of programming style. We will explore these in more detail in
section 1.3.2.

1.2.3 Models
So far, we have discussed a number of ways of framing the problem of designing and construct-
ing mechanisms. Each PCAP system is accompanied by a modeling system that lets us make
mathematical models of the systems we construct.
What is a model? It is a new system that is considerably simpler than the system being modeled,
but which captures the important aspects of the original system. We might make a physical model
of an airplane to test in a wind tunnel. It does not have to have passenger seats or landing lights;
but it has to have surfaces of the correct shape, in order to capture the important properties for
this purpose. Mathematical models can capture properties of systems that are important to us,
and allow us to discover things about the system much more conveniently than by manipulating
the original system itself.
One of the hardest things about building models is deciding which aspects of the original system
to model and which ones to ignore. Many classic, dramatic engineering failures can be ascribed to
failing to model important aspects of the system. One example (which turned out to be an ethical
success story, rather than a failure) is LeMessurier’s Citicorp Building 2, in which an engineering
design change was made, but tested in a model in which the wind came only from a limited set
of directions.
Another important dimension in modeling is whether the model is deterministic or not. We
might, for example, model the effect of the robot executing a command to set its velocity as mak-
ing an instantaneous change to the commanded velocity. But, of course, there is likely to be some
delay and some error. We have to decide whether to ignore that delay and error in our model and
treat it as if it were ideal; or, for example, to make a probabilistic model of the possible results
of that command. Generally speaking, the more different the possible outcomes of a particular
action or command, and the more diffuse the probability distribution over those outcomes, the
more important it is to explicitly include uncertainty in the model.
Once we have a model, we can use it in a variety of different ways, which we explore below.

Analytical models
By far the most prevalent use of models is in analysis: Given a circuit design, what will the voltage
on this terminal be? Given a computer program, will it compute the desired answer? How long
will it take? Given a control system, implemented as a circuit or as a program, will the system
stop at the right distance from the light bulb?

2
See “The Fifty-Nine Story Crisis”, The New Yorker, May 29, 1995, pp 45-53.
Chapter 1 Course Overview 6.01— Spring 2011— February 1, 2011 11

Analytical tools are important. It can be hard to verify the correctness of a system by trying
it in all possible initial conditions with all possible inputs; sometimes it is easier to develop a
mathematical model and then prove a theorem about the model.
For some systems, such as pure software computations, or the addition circuitry in a calculator,
it is possible to analyze correctness or speed with just a model of the system in question. For
other systems, such as fuel injectors, it is impossible to analyze the correctness of the controller
without also modeling the environment (or “plant”) to which it is connected, and then analyzing
the behavior of the coupled system of the controller and environment.
To demonstrate some of these tradeoffs, we can do a very simple analysis of a robot moving to-
ward a lamp. Imagine that we arrange it so that the robot’s velocity at time t, V[t], is proportional
to the difference between the actual light level, X[t], and a desired light level (that is, the light
level we expect to encounter when the robot is the desired distance from the lamp), Xdesired ; that
is, we can model our control system with the difference equation

V[t] = k(Xdesired − X[t])

where k is the constant of proportionality, or gain, of the controller.


Now, we need to model the world. For simplicity, we equate the light level to the robot’s position
(assuming that the units match); in addition we assume the robot’s position at time t is its position
at time t − 1 plus its velocity at time t − 1 (actually, this should be the product of the velocity and
the length of time between samples, but we can just assume a unit time step and thus use velocity).
When we couple these models, we get the difference equation

X[t] = X[t − 1] + k(Xdesired − X[t − 1]) .

Now, for a given value of k, we can determine how the system will behave over time, by solving
the difference equation in X.
Later in the course, we will see how easily-determined mathematical properties of a difference-
equation model can tell us whether a control system will have the desired behavior and whether
or not the controller will cause the system to oscillate. These same kinds of analyses can be applied
to robot control systems as well as to the temporal properties of voltages in a circuit, and even to
problems as unrelated as the consequences of a monetary policy decision in economics. It is
important to note that treating the system as moving in discrete time steps is an approximation
to underlying continuous dynamics; it can make models that are easier to analyze, but it requires
sophisticated understanding of sampling to understand the effects of this approximation on the
correctness of the results.

Synthetic models
One goal of many people in a variety of sub-disciplines of computer science and electrical en-
gineering is automatic synthesis of systems from formal descriptions of their desired behavior.
For example, you might describe some properties you would want the input/output behavior of
a circuit or computer program to have, and then have a computer system discover a circuit or
program with the desired property.
This is a well-specified problem, but generally the search space of possible circuits or programs
is much too large for an automated process; the intuition and previous experience of an expert
human designer cannot yet be matched by computational search methods.
Chapter 1 Course Overview 6.01— Spring 2011— February 1, 2011 12

However, humans use informal models of systems for synthesis. The documentation of a software
library, which describes the function of the various procedures, serves as an informal model that
a programmer can use to assemble those components to build new, complex systems.

Internal models
As we wish to build more and more complex systems with software programs as controllers, we
find that it is often useful to build additional layers of abstraction on top of the one provided by
a generic programming language. This is particularly true when the nature of the exact computa-
tion required depends considerably on information that is only received during the execution of
the system.
Consider, for example, an automated taxi robot (or, more prosaically, the navigation system in
your new car). It takes, as input, a current location in the city and a goal location, and gives, as
output, a path from the current location to the goal. It has a map built into it.
It is theoretically possible to build an enormous digital circuit or computer program that contains
a look-up table, in which we precompute the path for all pairs of locations and store them away.
Then when we want to find a path, we could simply look in the table at the location correspond-
ing to the start and goal location and retrieve the precomputed path. Unfortunately, the size of
that table is too huge to contemplate. Instead, what we do is construct an abstraction of the prob-
lem as one of finding a path from any start state to any goal state, through a graph (an abstract
mathematical construct of “nodes” with “arcs” connecting them). We can develop and implement
a general-purpose algorithm that will solve any shortest-path problem in a graph. That algorithm
and implementation will be useful for a wide variety of possible problems. And for the naviga-
tion system, all we have to do is represent the map as a graph, specify the start and goal states,
and call the general graph-search algorithm.
We can think of this process as actually putting a model of the system’s interactions with the
world inside the controller; it can consider different courses of action in the world and pick the
one that is the best according to some criterion.
Another example of using internal models is when the system has some significant lack of infor-
mation about the state of the environment. In such situations, it may be appropriate to explicitly
model the set of possible states of the external world and their associated probabilities, and to
update this model over time as new evidence is gathered. We will pursue an example of this
approach near the end of the course.

1.3 Programming embedded systems


There are many different ways of thinking about modularity and abstraction for software. Differ-
ent models will make some things easier to say and do and others more difficult, making different
models appropriate for different tasks. In the following sections, we explore different strate-
gies for building software systems that interact with an external environment, and then different
strategies for specifying the purely computational part of a system.

1.3.1 Interacting with the environment


Increasingly, computer systems need to interact with the world around them, receiving informa-
tion about the external world, and taking actions to affect the world. Furthermore, the world is
Chapter 1 Course Overview 6.01— Spring 2011— February 1, 2011 13

dynamic, so that as the system is computing, the world is changing, requiring future computation
to adapt to the new state of the world.
There are a variety of different ways to organize computations that interact with an external
world. Generally speaking, such a computation needs to:
1. get information from sensors (light sensors, sonars, mouse, keyboard, etc.),
2. perform computation, remembering some of the results, and
3. take actions to change the outside world (move the robot, print, draw, etc.).
These operations can be put together in different styles.

1.3.1.1 Sequential
The most immediately straightforward style for constructing a program that interacts with the
world is the basic imperative style, in which the program gives a sequence of ’commands’ to the
system it is controlling. A library of special procedures is defined, some of which read information
from the sensors and others of which cause actions to be performed. Example procedures might
move a robot forward a fixed distance, or send a file out over the network, or move video-game
characters on the screen.
In this model, we could naturally write a program that moves an idealized robot in a square, if
there is space in front of it.

if noObstacleInFront:
moveDistance(1)
turnAngle(90)
moveDistance(1)
turnAngle(90)
moveDistance(1)
turnAngle(90)
moveDistance(1)
turnAngle(90)

The major problem with this style of programming is that the programmer has to remember to
check the sensors sufficiently frequently. For instance, if the robot checks for free space in front,
and then starts moving, it might turn out that a subsequent sensor reading will show that there
is something in front of the robot, either because someone walked in front of it or because the
previous reading was erroneous. It is hard to have the discipline, as a programmer, to remember
to keep checking the sensor conditions frequently enough, and the resulting programs can be
quite difficult to read and understand.
For the robot moving toward the light, we might write a program like this:

while lightValue < desiredValue:


moveDistance(0.1)

This would have the robot creep up, step by step, toward the light. We might want to modify it so
that the robot moved a distance that was related to the difference between the current and desired
light values. However, if it takes larger steps, then during the time that it is moving it will not be
sensitive to possible changes in the light value and cannot react immediately to them.
Chapter 1 Course Overview 6.01— Spring 2011— February 1, 2011 14

1.3.1.2 Event-Driven
User-interface programs are often best organized differently, as event-driven (also called interrupt
driven) programs. In this case, the program is specified as a collection of procedures (called ’han-
dlers’ or ’callbacks’) that are attached to particular events that can take place. So, for example,
there might be procedures that are called when the mouse is clicked on each button in the inter-
face, when the user types into a text field, or when the temperature of a reactor gets too high.
An “event loop” runs continuously, checking to see whether any of the triggering events have
happened, and, if they have, calling the associated procedure.
In our simple example, we might imagine writing a program that told the robot to drive forward
by default; and then install an event handler that says that if the light level reaches the desired
value, it should stop. This program would be reactive to sudden changes in the environment.
As the number and frequency of the conditions that need responses increases, it can be difficult
to both keep a program like this running well and guarantee a minimum response time to any
event.

1.3.1.3 Transducer
An alternative view is that programming a system that interacts with an external world is like
building a transducer with internal state. Think of a transducer as a processing box that runs
continuously. At regular intervals (perhaps many times per second), the transducer reads all of
the sensors, does a small amount of computation, stores some values it will need for the next
computation, and then generates output values for the actions.
This computation happens over and over and over again. Complex behavior can arise out of
the temporal pattern of inputs and outputs. So, for example, a robot might try to move forward
without hitting something by defining a procedure:

def step():
distToFront = min(frontSonarReadings)
motorOutput(gain * (distToFront - desiredDistance), 0.0)

Executed repeatedly, this program will automatically modulate the robot’s speed to be propor-
tional to the free space in front of it.
The main problem with the transducer approach is that it can be difficult to do tasks that are
fundamentally sequential (like the example of driving in a square, shown above). We will start
with the transducer model, and then, as described in section 4.3, we will build a new abstraction
layer on top of it that will make it easy to do sequential commands, as well.

1.3.2 Programming models


Just as there are different strategies for organizing entire software systems, there are different
strategies for formally expressing computational processes within those structures.

1.3.2.1 Imperative computation


Most of you have probably been exposed to an imperative model of computer programming,
in which we think of programs as giving a sequential set of instructions to the computer to do
something. And, in fact, that is how the internals of the processors of computers are typically
Chapter 1 Course Overview 6.01— Spring 2011— February 1, 2011 15

structured. So, in Java or C or C++, you write typically procedures that consist of lists of instruc-
tions to the computer:
1. Put this value in this variable
2. Square the variable
3. Divide it by pi
4. If the result is greater than 1, return the result
In this model of computation, the primitive computational elements are basic arithmetic opera-
tions and assignment statements. We can combine the elements using sequences of statements,
and control structures such as if, for, and while. We can abstract away from the details of a
computation by defining a procedure that does it. Now, the engineer only needs to know the
specifications of the procedure, but not the implementation details, in order to use it.

1.3.2.2 Functional computation


Another style of programming is the functional style. In this model, we gain power through
function calls. Rather than telling the computer to do things, we ask it questions: What is 4 + 5?
What is the square root of 6? What is the largest element of the list?
These questions can all be expressed as asking for the value of a function applied to some argu-
ments. But where do the functions come from? The answer is, from other functions. We start with
some set of basic functions (like “plus”), and use them to construct more complex functions.
This method would not be powerful without the mechanisms of conditional evaluation and re-
cursion. Conditional functions ask one question under some conditions and another question
under other conditions. Recursion is a mechanism that lets the definition of a function refer to the
function being defined. Recursion is as powerful as iteration.
In this model of computation, the primitive computational elements are typically basic arithmetic
and list operations. We combine elements using function composition (using the output of one
function as the input to another), if, and recursion. We use function definition as a method
of abstraction, and the idea of higher-order functions (passing functions as arguments to other
functions) as a way of capturing common high-level patterns.

1.3.2.3 Data structures


In either style of asking the computer to do work for us, we have another kind of modularity and
abstraction, which is centered around the organization of data.
At the most primitive level, computers operate on collections of (usually 32 or 64) bits. We can
interpret such a collection of bits as representing different things: a positive integer, a signed
integer, a floating-point number, a Boolean value (true or false), one or more characters, or an
address of some other data in the memory of the computer. Python gives us the ability to work
directly with all of these primitives, except for addresses.
There is only so much you can do with a single number, though. We would like to build computer
programs that operate on representations of documents or maps or circuits or social networks. To
do so, we need to aggregate primitive data elements into more complex data structures. These can
include lists, arrays, dictionaries, and other structures of our own devising.
Here, again, we gain the power of abstraction. We can write programs that do operations on
a data structure representing a social network, for example, without having to worry about the
details of how the social network is represented in terms of bits in the machine.
Chapter 1 Course Overview 6.01— Spring 2011— February 1, 2011 16

1.3.2.4 Object-oriented programming: computation + data structures


Object-oriented programming is a style that applies the ideas of modularity and abstraction to
execution and data at the same time.
An object is a data structure, together with a set of procedures that operate on the data. Basic
procedures can be written in an imperative or a functional style, but ultimately there is imperative
assignment to state variables in the object.
One major new type of abstraction in OO programming is “generic” programming. It might be
that all objects have a procedure called print associated with them. So, we can ask any object to
print itself, without having to know how it is implemented. Or, in a graphics system, we might
have a variety of different objects that know their x, y positions on the screen. So each of them can
be asked, in the same way, to say what their position is, even though they might be represented
very differently inside the objects.
In addition, most object-oriented systems support inheritance, which is a way to make new kinds
of objects by saying that they are mostly like another kind of object, but with some exceptions.
This is another way to take advantage of abstraction.

Programming languages
Python as well as other modern programming languages, such as Java, Ruby and C++, support
all of these programming models. The programmer needs to choose which programming model
best suits the task. This is an issue that we will return to throughout the course.

1.4 Summary
We hope that this course will give you a rich set of conceptual tools and practical techniques, as
well as an appreciation of how math, modeling, and implementation can work together to enable
the design and analysis of complex computational systems that interact with the world.
Thanks to Jacob White, Hal Abelson, Tomas Lozano-Perez, Sarah Finney, Sari Canelake, Eric
Grimson, and Ike Chuang for useful comments and criticisms, and to Denny Freeman for sig-
nificant parts of the linear systems chapter.
Chapter 2 Learning to Program in Python 6.01— Spring 2011— February 1, 2011 17

Chapter 2
Learning to Program in Python

Depending on your previous programming background, we recommend different paths through


the available readings:
• If you have never programmed before: you should start with a general introduction to pro-
gramming and Python. We recommend Python for Software Design: How to Think Like a
Computer Scientist, by Allen Downey. This is a good introductory text that uses Python to
present basic ideas of computer science and programming. It is available for purchase in
hardcopy, or as a free download from:

http://www.greenteapress.com/thinkpython

After that, you can go straight to the next chapter.


• If you have programmed before, but not in Python: you should read the rest of this chapter
for a quick overview of Python, and how it may differ from other programming languages
with which you are familiar.
• If you have programmed in Python: you should skip to the next chapter.
Everyone should have a bookmark in their browser for Python Tutorial, by Guido Van Rossum.
This is the standard tutorial reference by the inventor of Python. It is accessible at:

http://docs.python.org/tut/tut.html

In the rest of this chapter, we will assume you know how to program in some language, but are
new to Python. We will use Java as an informal running comparative example. In this section we
will cover what we think are the most important differences between Python and what you may
already know about programming; but these notes are by no means complete.

2.1 Using Python


Python is designed for easy interaction between a user and the computer. It comes with an in-
teractive mode called a listener or shell. The shell gives a prompt (usually something like >>>)
and waits for you to type in a Python expression or program. Then it will evaluate the expression
you entered, and print out the value of the result. So, for example, an interaction with the Python
shell might look like this:

>>> 5 + 5
10
>>> x = 6
>>> x
6
>>> x + x
Chapter 2 Learning to Program in Python 6.01— Spring 2011— February 1, 2011 18

12
>>> y = ’hi’
>>> y + y
’hihi’
>>>

So, you can use Python as a fancy calculator. And as you define your own procedures in Python,
you can use the shell to test them or use them to compute useful results.

2.1.1 Indentation and line breaks


Every programming language has to have some method for indicating grouping of instructions.
Here is how you write an if-then-else structure in Java:

if (s == 1){
s = s + 1;
a = a - 10;
} else {
s = s + 10;
a = a + 10;
}

The braces specify what statements are executed in the if case. It is considered good style to
indent your code to agree with the brace structure, but it is not required. In addition, the semi-
colons are used to indicate the end of a statement, independent of the locations of the line breaks
in the file. So, the following code fragment has the same meaning as the previous one, although
it is much harder to read and understand.

if (s == 1){
s = s
+ 1; a = a - 10;
} else {
s = s + 10;
a = a + 10;
}

In Python, on the other hand, there are no braces for grouping or semicolons for termination.
Indentation indicates grouping and line breaks indicate statement termination. So, in Python, we
would write the previous example as

if s == 1:
s = s + 1
a = a - 10
else:
s = s + 10
a = a + 10

There is no way to put more than one statement on a single line. 3 If you have a statement that is
too long for a line, you can signal it with a backslash:

aReallyLongVariableNameThatMakesMyLinesLong = \
aReallyLongVariableNameThatMakesMyLinesLong + 1

3
Actually, you can write something like if a > b: a = a + 1 all on one line, if the work you need to do inside an
if or a for is only one line long.
Chapter 2 Learning to Program in Python 6.01— Spring 2011— February 1, 2011 19

It is easy for Java programmers to get confused about colons and semi-colons in Python. Here is
the deal: (1) Python does not use semi-colons; (2) Colons are used to start an indented block, so
they appear on the first line of a procedure definition, when starting a while or for loop, and
after the condition in an if, elif, or else.
Is one method better than the other? No. It is entirely a matter of taste. The Python method is
pretty unusual. But if you are going to use Python, you need to remember that indentation and
line breaks are significant.

2.1.2 Types and declarations


Java programs are what is known as statically and strongly typed. Thus, the types of all the
variables must be known at the time that the program is written. This means that variables have
to be declared to have a particular type before they are used. It also means that the variables
cannot be used in a way that is inconsistent with their type. So, for instance, you would declare x
to be an integer by saying

int x;
x = 6 * 7;

But you would get into trouble if you left out the declaration, or did

int x;
x = "thing";

because a type checker is run on your program to make sure that you don’t try to use a variable
in a way that is inconsistent with its declaration.
In Python, however, things are a lot more flexible. There are no variable declarations, and the
same variable can be used at different points in your program to hold data objects of different
types. So, the following is fine, in Python:

if x == 1:
x = 89.3
else:
x = "thing"

The advantage of having type declarations and compile-time type checking, as in Java, is that a
compiler can generate an executable version of your program that runs very quickly, because it
can be certain about what kind of data is stored in each variable, and it does not have to check it at
runtime. An additional advantage is that many programming mistakes can be caught at compile
time, rather than waiting until the program is being run. Java would complain even before your
program started to run that it could not evaluate

3 + "hi"

Python would not complain until it was running the program and got to that point.
The advantage of the Python approach is that programs are shorter and cleaner looking, and pos-
sibly easier to write. The flexibility is often useful: In Python, it is easy to make a list or array with
objects of different types stored in it. In Java, it can be done, but it is trickier. The disadvantage
of the Python approach is that programs tend to be slower. Also, the rigor of compile-time type
checking may reduce bugs, especially in large programs.
Chapter 2 Learning to Program in Python 6.01— Spring 2011— February 1, 2011 20

2.1.3 Modules
As you start to write bigger programs, you will want to keep the procedure definitions in multiple
files, grouped together according to what they do. So, for example, we might package a set of
utility functions together into a single file, called utility.py. This file is called a module in
Python.
Now, if we want to use those procedures in another file, or from the the Python shell, we will
need to say

import utility

so that all those procedures become available to us and to the Python interpereter. Now, if we
have a procedure in utility.py called foo, we can use it with the name utility.foo. You can
read more about modules, and how to reference procedures defined in modules, in the Python
documentation.

2.1.4 Interaction and Debugging


We encourage you to adopt an interactive style of programming and debugging. Use the Python
shell a lot. Write small pieces of code and test them. It is much easier to test the individual pieces
as you go, rather than to spend hours writing a big program, and then find it does not work, and
have to sift through all your code, trying to find the bugs.
But, if you find yourself in the (inevitable) position of having a big program with a bug in it, do
not despair. Debugging a program does not require brilliance or creativity or much in the way of
insight. What it requires is persistence and a systematic approach.
First of all, have a test case (a set of inputs to the procedure you are trying to debug) and know
what the answer is supposed to be. To test a program, you might start with some special cases:
what if the argument is 0 or the empty list? Those cases might be easier to sort through first (and
are also cases that can be easy to get wrong). Then try more general cases.
Now, if your program gets your test case wrong, what should you do? Resist the temptation to
start changing your program around, just to see if that will fix the problem. Do not change any
code until you know what is wrong with what you are doing now, and therefore believe that the
change you make is going to correct the problem.
Ultimately, for debugging big programs, it is most useful to use a software development environ-
ment with a serious debugger. But these tools can sometimes have a steep learning curve, so in
this class we will learn to debug systematically using “print” statements.
One good way to use “print” statements to help in debugging is to use a variation on binary
search. Find a spot roughly halfway through your code at which you can predict the values of
variables, or intermediate results your computation. Put a print statement there that lists expected
as well as actual values of the variables. Run your test case, and check. If the predicted values
match the actual ones, it is likely that the bug occurs after this point in the code; if they do not,
then you have a bug prior to this point (of course, you might have a second bug after this point,
but you can find that later). Now repeat the process by finding a location halfway between the
beginning of the procedure and this point, placing a print statement with expected and actual
values, and continuing. In this way you can narrow down the location of the bug. Study that part
of the code and see if you can see what is wrong. If not, add some more print statements near the
problematic part, and run it again. Don’t try to be smart....be systematic and indefatigable!
Chapter 2 Learning to Program in Python 6.01— Spring 2011— February 1, 2011 21

You should learn enough of Python to be comfortable writing basic programs, and to be able to
efficiently look up details of the language that you don’t know or have forgotten.

2.2 Procedures
In Python, the fundamental abstraction of a computation is as a procedure (other books call them
“functions” instead; we will end up using both terms). A procedure that takes a number as an
argument and returns the argument value plus 1 is defined as:

def f(x):
return x + 1

The indentation is important here, too. All of the statements of the procedure have to be indented
one level below the def. It is crucial to remember the return statement at the end, if you want
your procedure to return a value. So, if you defined f as above, then played with it in the shell, 4
you might get something like this:

>>> f
<function f at 0x82570>
>>> f(4)
5
>>> f(f(f(4)))
7

If we just evaluate f, Python tells us it is a function. Then we can apply it to 4 and get 5, or apply
it multiple times, as shown.
What if we define

def g(x):
x + 1

Now, when we play with it, we might get something like this:

>>> g(4)
>>> g(g(4))
Traceback (most recent call last):
File "<stdin>", line 1, in ?
File "<stdin>", line 2, in g
TypeError: unsupported operand type(s) for +: ’NoneType’ and ’int’

What happened!! First, when we evaluated g(4), we got nothing at all, because our definition of
g did not return anything. Well...strictly speaking, it returned a special value called None, which
the shell does not bother printing out. The value None has a special type, called NoneType. So,
then, when we tried to apply g to the result of g(4), it ended up trying to evaluate g(None),
which made it try to evaluate None + 1, which made it complain that it did not know how to
add something of type NoneType and something of type int.

4
Although you can type procedure definitions directly into the shell, you will not want to work that way, because if
there is a mistake in your definition, you will have to type the whole thing in again. Instead, you should type your
procedure definitions into a file, and then get Python to evaluate them. Look at the documentation for Idle or the 6.01
FAQ for an explanation of how to do that.
Chapter 2 Learning to Program in Python 6.01— Spring 2011— February 1, 2011 22

Whenever you ask Python to do something it cannot do, it will complain. You should learn to
read the error messages, because they will give you valuable information about what is wrong
with what you were asking.

Print vs Return
Here are two different function definitions:

def f1(x):
print x + 1
def f2(x):
return x + 1

What happens when we call them?

>>> f1(3)
4
>>> f2(3)
4

It looks like they behave in exactly the same way. But they don’t, really. Look at this example:

>>> print(f1(3))
4
None
>>> print(f2(3))
4

In the case of f1, the function, when evaluated, prints 4; then it returns the value None, which is
printed by the Python shell. In the case of f2, it does not print anything, but it returns 4, which is
printed by the Python shell. Finally, we can see the difference here:

>>> f1(3) + 1
4
Traceback (most recent call last):
File "<stdin>", line 1, in ?
TypeError: unsupported operand type(s) for +: ’NoneType’ and ’int’
>>> f2(3) + 1
5

In the first case, the function does not return a value, so there is nothing to add to 1, and an error
is generated. In the second case, the function returns the value 4, which is added to 1, and the
result, 5, is printed by the Python read-eval-print loop.
The book Think Python, which we recommend reading, was translated from a version for Java, and
it has a lot of print statements in it, to illustrate programming concepts. But for just about every-
thing we do, it will be returned values that matter, and printing will be used only for debugging,
or to give information to the user.
Print is very useful for debugging. It is important to know that you can print out as many items
as you want in one line:

>>> x = 100
>>> print ’x’, x, ’x squared’, x*x, ’xiv’, 14
x 100 x squared 10000 xiv 14
Chapter 2 Learning to Program in Python 6.01— Spring 2011— February 1, 2011 23

We have also snuck in another data type on you: strings. A string is a sequence of characters. You
can create a string using single or double quotes; and access individual elements of strings using
indexing.

>>> s1 = ’hello world’


>>> s2 = "hello world"
>>> s1 == s2
True
>>> s1[3]
’l’

As you can see, indexing refers to the extraction of a particular element of a string, by using
square brackets [i] where i is a number that identifies the location of the character that you
wish to extract (note that the indexing starts with 0 ).
Look in the Python documentation for more about strings.

2.3 Control structures


Python has control structures that are slightly different from those in other languages.

2.3.1 Conditionals

Booleans
Before we talk about conditionals, we need to clarify the Boolean data type. It has values True
and False. Typical expressions that have Boolean values are numerical comparisons:

>>> 7 > 8
False
>>> -6 <= 9
True

We can also test whether data items are equal to one another. Generally we use == to test for
equality. It returns True if the two objects have equal values. Sometimes, however, we will be
interested in knowing whether the two items are the exact same object (in the sense discussed in
section 3.3). In that case we use is:

>>> [1, 2] == [1, 2]


True
>>> [1, 2] is [1, 2]
False
>>> a = [1, 2]
>>> b = [1, 2]
>>> c = a
>>> a == b
True
>>> a is b
False
>>> a == c
True
>>> a is c
True
Chapter 2 Learning to Program in Python 6.01— Spring 2011— February 1, 2011 24

Thus, in the examples above, we see that == testing can be applied to nested structures, and basi-
cally returns true if each of the individual elements is the same. However, is testing, especially
when applied to nested structures, is more refined, and only returns True if the two objects point
to exactly the same instance in memory.
In addition, we can combine Boolean values conveniently using and, or, and not:

>>> 7 > 8 or 8 > 7


True
>>> not 7 > 8
True
>>> 7 == 7 and 8 > 7
True

If
Basic conditional statements have the form: 5

if <booleanExpr>:
<statementT1>
...
<statementTk>
else:
<statementF1>
...
<statementFn>

When the interpreter encounters a conditional statement, it starts by evaluating <boolean-


Expr>, getting either True or False as a result. 6 If the result is True, then it will eval-
uate <statementT1>,...,<statementTk>; if it is False, then it will evaluate <state-
mentF1>,...,<statementFn>. Crucially, it always evaluates only one set of the statements.
Now, for example, we can implement a procedure that returns the absolute value of its argument.

def abs(x):
if x >= 0:
return x
else:
return -x

We could also have written

def abs(x):
if x >= 0:
result = x
else:
result = -x
return result

Python uses the level of indentation of the statements to decide which ones go in the groups of
statements governed by the conditionals; so, in the example above, the return result statement
is evaluated once the conditional is done, no matter which branch of the conditional is evaluated.

5
See the Python documentation for more variations.
6
In fact, Python will let you put any expression in the place of <booleanExpr>, and it will treat the values 0, 0.0, [],
’’, and None as if they were False and everything else as True.
Chapter 2 Learning to Program in Python 6.01— Spring 2011— February 1, 2011 25

For and While


If we want to do some operation or set of operations several times, we can manage the process in
several different ways. The most straightforward are for and while statements (often called for
and while loops).
A for loop has the following form:

for <var> in <listExpr>:


<statement1>
...
<statementn>

The interpreter starts by evaluating listExpr. If it does not yield a list, tuple, or string 7, an error
occurs. If it does yield a list or list-like structure, then the block of statements will, under normal
circumstances, be executed one time for every value in that list. At the end, the variable <var>
will remain bound to the last element of the list (and if it had a useful value before the for was
evaluated, that value will have been permanently overwritten).
Here is a basic for loop:

result = 0
for x in [1, 3, 4]:
result = result + x * x

At the end of this execution, result will have the value 26, and x will have the value 4.
One situation in which the body is not executed once for each value in the list is when a return
statement is encountered. No matter whether return is nested in a loop or not, if it is evaluated it
immediately causes a value to be returned from a procedure call. So, for example, we might write
a procedure that tests to see if an item is a member of a list, and returns True if it is and False if
it is not, as follows:

def member(x, items):


for i in items:
if x == i:
return True
return False

The procedure loops through all of the elements in items, and compares them to x. As soon as
it finds an item i that is equal to x, it can quit and return the value True from the procedure. If
it gets all the way through the loop without returning, then we know that x is not in the list, and
we can return False.

Exercise 2.1. Write a procedure that takes a list of numbers, nums, and a limit, limit,
and returns a list which is the shortest prefix of nums the sum of whose
values is greater than limit. Use for. Try to avoid using explicit indexing
into the list. (Hint: consider the strategy we used in member.)

7
or, more esoterically, another object that can be iterated over.
Chapter 2 Learning to Program in Python 6.01— Spring 2011— February 1, 2011 26

Range
Very frequently, we will want to iterate through a list of integers, often as indices. Python provides
a useful procedure, range, which returns lists of integers. It can be used in complex ways, but the
basic usage is range(n), which returns a list of integers going from 0 up to, but not including, its
argument. So range(3) returns [0, 1, 2].

Exercise 2.2. Write a procedure that takes n as an argument and returns the sum of the
squares of the integers from 1 to n-1. It should use for and range.

Exercise 2.3. What is wrong with this procedure, which is supposed to return True if
the element x occurs in the list items, and False otherwise?

def member (x, items):


for i in items:
if x == i:
return True
else:
return False

While
You should use for whenever you can, because it makes the structure of your loops clear. Some-
times, however, you need to do an operation several times, but you do not know in advance how
many times it needs to be done. In such situations, you can use a while statement, of the form:

while <booleanExpr>:
<statement1>
...
<statementn>

In order to evaluate a while statement, the interpreter evaluates <booleanExpr>, getting a


Boolean value. If the value is False, it skips all the statements and evaluation moves on to the
next statement in the program. If the value is True, then the statements are executed, and the
<booleanExpr> is evaluated again. If it is False, execution of the loop is terminated, and if it is
True, it goes around again.
It will generally be the case that you initialize a variable before the while statement, change that
variable in the course of executing the loop, and test some property of that variable in the Boolean
expression. Imagine that you wanted to write a procedure that takes an argument n and returns
the largest power of 2 that is smaller than n. You might do it like this:

def pow2Smaller(n):
p = 1
while p*2 < n:
p = p*2
return p
Chapter 2 Learning to Program in Python 6.01— Spring 2011— February 1, 2011 27

Lists
Python has a built-in list data structure that is easy to use and incredibly convenient. So, for
instance, you can say

>>> y = [1, 2, 3]
>>> y[0]
1
>>> y[2]
3
>>> y[-1]
3
>>> y[-2]
2
>>> len(y)
3
>>> y + [4]
[1, 2, 3, 4]
>>> [4] + y
[4, 1, 2, 3]
>>> [4,5,6] + y
[4, 5, 6, 1, 2, 3]
>>> y
[1, 2, 3]

A list is written using square brackets, with entries separated by commas. You can get elements
out by specifying the index of the element you want in square brackets, but note that, like for
strings, the indexing starts with 0. Note that you can index elements of a list starting from the
initial (or zeroth) one (by using integers), or starting from the last one (by using negative integers).
You can add elements to a list using ’+’, taking advantage of Python operator overloading. Note
that this operation does not change the original list, but makes a new one.
Another useful thing to know about lists is that you can make slices of them. A slice of a list is
sublist; you can get the basic idea from examples.

>>> b = range(10)
>>> b
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> b[1:]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> b[3:]
[3, 4, 5, 6, 7, 8, 9]
>>> b[:7]
[0, 1, 2, 3, 4, 5, 6]
>>> b[:-1]
[0, 1, 2, 3, 4, 5, 6, 7, 8]
>>> b[:-2]
[0, 1, 2, 3, 4, 5, 6, 7]
Chapter 2 Learning to Program in Python 6.01— Spring 2011— February 1, 2011 28

Iteration over lists


What if you had a list of integers, and you wanted to add them up and return the sum? Here are
a number of different ways of doing it. 8
First, here is a version in a style you might have learned to write in a Java class (actually, you
would have used for, but Python does not have a for that works like the one in C and Java).

def addList1(l):
sum = 0
listLength = len(l)
i = 0
while (i < listLength):
sum = sum + l[i]
i = i + 1
return sum

It increments the index i from 0 through the length of the list - 1, and adds the appropriate element
of the list into the sum. This is perfectly correct, but pretty verbose and easy to get wrong.
Here is a version using Python’s for loop.

def addList2(l):
sum = 0
for i in range(len(l)):
sum = sum + l[i]
return sum

A loop of the form

for x in l:
something

will be executed once for each element in the list l, with the variable x containing each successive
element in l on each iteration. So,

for x in range(3):
print x

will print 0 1 2. Back to addList2, we see that i will take on values from 0 to the length of the
list minus 1, and on each iteration, it will add the appropriate element from l into the sum. This
is more compact and easier to get right than the first version, but still not the best we can do!
This one is even more direct.

def addList3(l):
sum = 0
for v in l:

8
For any program you will ever need to write, there will be a huge number of different ways of doing it. How should
you choose among them? The most important thing is that the program you write be correct, and so you should choose
the approach that will get you to a correct program in the shortest amount of time. That argues for writing it in the way
that is cleanest, clearest, shortest. Another benefit of writing code that is clean, clear and short is that you will be better
able to understand it when you come back to it in a week or a month or a year, and that other people will also be better
able to understand it. Sometimes, you will have to worry about writing a version of a program that runs very quickly,
and it might be that in order to make that happen, you will have to write it less cleanly or clearly or briefly. But it is
important to have a version that is correct before you worry about getting one that is fast.
Chapter 2 Learning to Program in Python 6.01— Spring 2011— February 1, 2011 29

sum = sum + v
return sum

We do not ever really need to work with the indices. Here, the variable v takes on each successive
value in l, and those values are accumulated into sum.
For the truly lazy, it turns out that the function we need is already built into Python. It is called
sum:

def addList4(l):
return sum(l)

In section ??, we will see another way to do addList, which many people find more beautiful
than the methods shown here.

List Comprehensions
Python has a very nice built-in facility for doing many iterative operations, called list comprehen-
sions. The basic template is

[<resultExpr> for <var> in <listExpr> if <conditionExpr>]

where <var> is a single variable (or a tuple of variables), <listExpr> is an expression that evalu-
ates to a list, tuple, or string, and <resultExpr> is an expression that may use the variable <var>.
The if <conditionExpr> is optional; if it is present, then only those values of <var> for which
that expression is True are included in the resulting computation.
You can view a list comprehension as a special notation for a particular, very common, class of
for loops. It is equivalent to the following:

*resultVar* = []
for <var> in <listExpr>:
if <conditionExpr>:
*resultVar*.append(<resultExpr>)
*resultVar*

We used a kind of funny notation *resultVar* to indicate that there is some anonymous list that
is getting built up during the evaluation of the list comprehension, but we have no real way of
accessing it. The result is a list, which is obtained by successively binding <var> to elements of
the result of evaluating <listExpr>, testing to see whether they meet a condition, and if they
meet the condition, evaluating <resultExpr> and collecting the results into a list.
Whew. It is probably easier to understand it by example.

>>> [x/2.0 for x in [4, 5, 6]]


[2.0, 2.5, 3.0]
>>> [y**2 + 3 for y in [1, 10, 1000]]
[4, 103, 1000003]
>>> [a[0] for a in [[’Hal’, ’Abelson’],[’Jacob’,’White’],
[’Leslie’,’Kaelbling’]]]
[’Hal’, ’Jacob’, ’Leslie’]
>>> [a[0]+’!’ for a in [[’Hal’, ’Abelson’],[’Jacob’,’White’],
[’Leslie’,’Kaelbling’]]]
[’Hal!’, ’Jacob!’, ’Leslie!’]
Chapter 2 Learning to Program in Python 6.01— Spring 2011— February 1, 2011 30

Imagine that you have a list of numbers and you want to construct a list containing just the ones
that are odd. You might write

>>> nums = [1, 2, 5, 6, 88, 99, 101, 10000, 100, 37, 101]
>>> [x for x in nums if x%2==1]
[1, 5, 99, 101, 37, 101]

Note the use of the if conditional here to include only particular values of x.
And, of course, you can combine this with the other abilities of list comprehensions, to, for exam-
ple, return the squares of the odd numbers:

>>> [x*x for x in nums if x%2==1]


[1, 25, 9801, 10201, 1369, 10201]

You can also use structured assignments in list comprehensions

>>> [first for (first, last) in [[’Hal’, ’Abelson’],[’Jacob’,’White’],


[’Leslie’,’Kaelbling’]]]
[’Hal’, ’Jacob’, ’Leslie’]
>>> [first+last for (first, last) in [[’Hal’, ’Abelson’],[’Jacob’,’White’],
[’Leslie’,’Kaelbling’]]]
[’HalAbelson’, ’JacobWhite’, ’LeslieKaelbling’]

Another built-in function that is useful with list comprehensions is zip. Here are some examples
of how it works:

> zip([1, 2, 3],[4, 5, 6])


[(1, 4), (2, 5), (3, 6)]
> zip([1,2], [3, 4], [5, 6])
[(1, 3, 5), (2, 4, 6)]

Here is an example of using zip with a list comprehension:

>>> [first+last for (first, last) in zip([’Hal’, ’Jacob’, ’Leslie’],


[’Abelson’,’White’,’Kaelbling’])]
[’HalAbelson’, ’JacobWhite’, ’LeslieKaelbling’]

Note that this last example is very different from this one:

>>> [first+last for first in [’Hal’, ’Jacob’, ’Leslie’] \


for last in [’Abelson’,’White’,’Kaelbling’]]
[’HalAbelson’, ’HalWhite’, ’HalKaelbling’, ’JacobAbelson’, ’JacobWhite’,
’JacobKaelbling’, ’LeslieAbelson’, ’LeslieWhite’, ’LeslieKaelbling’]

Nested list comprehensions behave like nested for loops, the expression in the list comprehen-
sion is evaluated for every combination of the values of the variables.

2.4 Common Errors and Messages


Here are some common Python errors and error messages to watch out for. Please let us know if
you have any favorite additions for this list.
• A complaint about NoneType often means you forgot a return.
Chapter 2 Learning to Program in Python 6.01— Spring 2011— February 1, 2011 31

def plus1 (x):


x + 1
>>> y = plus1(x)
>>> plus1(x) + 2
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: ’NoneType’ and ’int’

• Weird results from math can result from integer division

>>> 3/ 9
0

• “Unsubscriptable object” means you are trying to get an element out of something that isn’t a
dictionary, list, or tuple.

>>> x = 1
>>> x[3]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: ’int’ object is unsubscriptable

• “Object is not callable” means you are trying to use something that isn’t a procedure or method
as if it were.

>>> x = 1
>>> x(3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: ’int’ object is not callable

• “List index out of range” means you are trying to read or write an element of a list that is not
present.

>>> a = range(5)
>>> a
[0, 1, 2, 3, 4]
>>> a[5]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list index out of range

• “Maximum recursion depth exceeded” means you have a recursive procedure that is nested
very deeply or your base case is not being reached due to a bug.

def fizz(x):
return fizz(x - 1)
>>> fizz(10)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in fizz
File "<stdin>", line 2, in fizz
...
File "<stdin>", line 2, in fizz
RuntimeError: maximum recursion depth exceeded
Chapter 2 Learning to Program in Python 6.01— Spring 2011— February 1, 2011 32

• “Key Error” means that you are trying to look up an element in a dictionary that is not present.

>>> d = {’a’:7, ’b’:8}


>>> d[’c’]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: ’c’

• Another common error is forgetting the self before calling a method. This generates the same
error that you would get if you tried to call a function that wasn’t defined at all.

Traceback (most recent call last):


File "<stdin>", line 1, in <module>
File "V2.py", line 22, in __add__
return add(v)
NameError: global name ’add’ is not defined

2.5 Python Style


Software engineering courses often provide very rigid guidelines on the style of programming,
specifying the appropriate amount of indentation, or what to capitalize, or whether to use un-
derscores in variable names. Those things can be useful for uniformity and readability of code,
especially when a lot of people are working on a project. But they are mostly arbitrary: a style is
chosen for consistency and according to some person’s aesthetic preferences.
There are other matters of style that seem, to us, to be more fundamental, because they directly
affect the readability or efficiency of the code.
• Avoid recalculation of the same value.

You should compute it once and assign it to a variable instead; otherwise, if you have a bug
in the calculation (or you want to change the program), you will have to change it multiple
times. It is also inefficient.
• Avoid repetition of a pattern of computation.

You should use a function instead, again to avoid having to change or debug the same basic
code multiple times.
• Avoid numeric indexing.

You should use destructuring if possible, since it is much easier to read the code and therefore
easier to get right and to modify later.
• Avoid excessive numeric constants.

You should name the constants, since it is much easier to read the code and therefore easier to
get right and to modify later.
Here are some examples of simple procedures that exhibit various flaws. We’ll talk about what
makes them problematic.

2.5.1 Normalize a vector


Let’s imagine we want to normalize a vector of three values; that is to compute a new vector of
three values, such that its length is 1. Here is our first attempt; it is a procedure that takes as input
a list of three numbers, and returns a list of three numbers:
Chapter 2 Learning to Program in Python 6.01— Spring 2011— February 1, 2011 33

def normalize3(v):
return [v[0]/math.sqrt(v[0]**2+v[1]**2+v[2]**2),
v[1]/math.sqrt(v[0]**2+v[1]**2+v[2]**2),
v[2]/math.sqrt(v[0]**2+v[1]**2+v[2]**2)]

This is correct, but it looks pretty complicated. Let’s start by noticing that we’re recalculating the
denominator three times, and instead save the value in a variable.

def normalize3(v):
magv = math.sqrt(v[0]**2+v[1]**2+v[2]**2)
return [v[0]/magv,v[1]/magv,v[2]/magv]

Now, we can see a repeated pattern, of going through and dividing each element by magv. Also,
we observe that the computation of the magnitude of a vector is a useful and understandable
operation in its own right, and should probably be put in its own procedure. That leads to this
procedure:

def mag(v):
return math.sqrt(sum([vi**2 for vi in v]))

def normalize3(v):
return [vi/mag(v) for vi in v]

This is especially nice, because now, in fact, it applies not just to vectors of length three. So, it’s
both shorter and more general than what we started with. But, one of our original problems has
snuck back in: we’re recomputing mag(v) once for each element of the vector. So, finally, here’s a
version that we’re very happy with: 9

def mag(v):
return math.sqrt(sum([vi**2 for vi in v]))

def normalize3(v):
magv = mag(v)
return [vi/magv for vi in v]

2.5.2 Perimeter of a polygon


Now, let’s consider the problem of computing the length of the perimeter of a polygon. The input
is a list of vertices, encoded as a list of lists of two numbers (such as [[1, 2], [3.4, 7.6],
[-4.4, 3]]). Here is our first attempt:

def perim(vertices):
result = 0
for i in range(len(vertices)-1):
result = result + math.sqrt((vertices[i][0]-vertices[i+1][0])**2 + \
(vertices[i][1]-vertices[i+1][1])**2)
return result + math.sqrt((vertices[-1][0]-vertices[0][0])**2 + \
(vertices[-1][1]-vertices[0][1])**2)

9
Note that there is still something for someone to be unhappy with here: the use of a list comprehension means that
we’re creating a new list, which we are just going to sum up; that’s somewhat less efficient than adding the values
up in a loop. However, as we said at the outset, for almost every program, clarity matters more than efficiency. And
once you have something that’s clear and correct, you can selectively make the parts that are executed frequently more
efficient.
Chapter 2 Learning to Program in Python 6.01— Spring 2011— February 1, 2011 34

Again, this works, but it ain’t pretty. The main problem is that someone reading the code doesn’t
immediately see what all that subtraction and squaring is about. We can fix this by defining
another procedure:

def perim(vertices):
result = 0
for i in range(len(vertices)-1):
result = result + pointDist(vertices[i],vertices[i+1])
return result + pointDist(vertices[-1],vertices[0])

def pointDist(p1,p2):
return math.sqrt(sum([(p1[i] - p2[i])**2 for i in range(len(p1))]))

Now, we’ve defined a new procedure pointDist, which computes the Euclidean distance be-
tween two points. And, in fact, we’ve written it generally enough to work on points of any
dimension (not just two). Just for fun, here’s another way to compute the distance, which some
people would prefer and others would not.

def pointDist(p1,p2):
return math.sqrt(sum([(c1 - c2)**2 for (c1, c2) in zip(p1, p2)]))

For this to make sense, you have to understand zip. Here’s an example of how it works:

> zip([1, 2, 3],[4, 5, 6])


[(1, 4), (2, 5), (3, 6)]

2.5.3 Bank transfer


What if we have two values, representing bank accounts, and want to transfer an amount of
money amt between them? Assume that a bank account is represented as a list of values, such as
[’Alyssa’, 8300343.03, 0.05], meaning that ’Alyssa’ has a bank balance of $8,300,343.03,
and has to pay a 5-cent fee for every bank transaction. We might write this procedure as follows.
It moves the amount from one balance to the other, and subtracts the transaction fee from each
account.

def transfer(a1, a2, amt):


a1[1] = a1[1] - amt - a1[2]
a2[1] = a2[1] + amt - a2[2]

To understand what it’s doing, you really have to read the code at a detailed level. Furthermore,
it’s easy to get the variable names and subscripts wrong.
Here’s another version that abstracts away the common idea of a deposit (which can be positive
or negative) into a procedure, and uses it twice:

def transfer(a1, a2, amt):


deposit(a1, -amt)
deposit(a2, amt)

def deposit(a, amt):


a[1] = a[1] + amt - a[2]
Chapter 2 Learning to Program in Python 6.01— Spring 2011— February 1, 2011 35

Now, transfer looks pretty clear, but deposit could still use some work. In particular, the use
of numeric indices to get the components out of the bank account definition is a bit cryptic (and
easy to get wrong). 10

def deposit(a, amt):


(name, balance, fee) = a
a[1] = balance + amt - fee

Here, we’ve used a destructuring assignment statement to give names to the components of the
account. Unfortunately, when we want to change an element of the list representing the account,
we still have to index it explicitly. Given that we have to use explicit indices, this approach in
which we name them might be better.

acctName = 0
acctBalance = 1
acctFee = 2
def deposit(a, amt):
a[acctBalance] = a[acctBalance] + amt - a[acctFee]

Strive, in your programming, to make your code as simple, clear, and direct as possible. Occa-
sionally, the simple and clear approach will be too inefficient, and you’ll have to do something
more complicated. In such cases, you should still start with something clear and simple, and in
the end, you can use it as documentation.

2.5.4 Coding examples


Following are some attempts at defining a procedure isSubset, which takes two arguments, a
and b, and returns True if a is a subset of b, assuming that a and b are represented as lists of
elements.
Here is one solution to the problem which is in the Pythonic functional style.

def isSubset(a, b):


return reduce(operator.and_, [x in b for x in a])

This is short and direct. The expression x in b tests to see if the item x is in the list b. So,
[x in b for x in a] is a list of Booleans, one for each element in a, indicating whether that
element is in b. Finally, we reduce that list using the and operator 11 , which will have the value
True if all of the elements of the list are True, and False otherwise.
An alternative is to do it recursively:

def isSubset(a, b):


if a == []:
return True
else:
return a[0] in b and isSubset(a[1:], b)

10
We’ll see other approaches to this when we start to look at object-oriented programming. But it’s important to apply
basic principles of naming and clarity no matter whether you’re using assembly language or Java.
11
To get versions of basic Python operations in the form of procedures, you need to do import operator. Now, you can
do addition with operator.add(3, 4). Because and already has special syntactic significance in Python, they had to
name the operator version of it something different, and so it is operator.and_.
Chapter 2 Learning to Program in Python 6.01— Spring 2011— February 1, 2011 36

The base case of the recursion is that a is the empty list; in that case, it’s clear that a is a subset of
b. Otherwise, we can define our answer in terms of isSubset, but asking a simpler question (in
this case, on a list for a which is one element smaller). So, we say that a is a subset of b if the first
element of a is a member of b and the set of rest of the elements of a is a subset of b.
We could go even farther in compressing the recursive solution:

def isSubset(a, b):


return a == None or a[0] in b and isSubset(a[1:], b)

Here, we are taking advantage of the fact that in Python (and most other languages), the or oper-
ator has the “early out” property. That is, if we are evaluating e1 or e2, we start by evaluating
e1, and if it is True, then we know the result of the expression has to be True, and therefore
we return without evaluating e2. So, or can act as a kind of conditional operator. Some people
would find this example too abstruse (shorter isn’t always better), and some would find it very
beautiful.
Here is another good solution, this time in the imperative style:

def isSubset(a, b):


for x in a:
if not x in b:
return False
return True

It works by going through the elements in a, in order. If any of those elements is not in b, then we
can immediately quit and return False. Otherwise, if we have gotten through all of the elements
in a, and each of them has been in b, then a really is a subset of b and we can return True.
Here is another good imperative example:

def isSubset(a, b):


result = True
for x in a:
result = result and x in b
return result

This procedure starts by initializing a result to True, which is the identity element for and (it
plays the same role as 0 for add). Then it goes all the way through the list a, and ands the old
result with x in b. This computation is very similar to the computation done in the functional
version, but rather than creating the whole list of Booleans and then reducing, we interleave the
individual membership tests with combining them into a result.
All of these examples are short and clear, and we’d be happy to see you write any of them. How-
ever, there are lots of versions of this procedure that students have written that we are not happy
with. Here are some examples of procedures that are correct, in the sense that they return the
right answer, but incorrect, in the sense that they are long or confused or hard to read.
Chapter 2 Learning to Program in Python 6.01— Spring 2011— February 1, 2011 37

Bad Example 1.

def isSubset(a,b):
list1=[]
for i in a:
for j in b:
if i==j:
list1.append(i)
break
if len(list1)==len(a):
return True
else:
return False

This procedure works by going through both lists and making a list of the
items that are in common (basically computing the intersection). Then, it
checks to see whether the intersection is of the same length as a. There are
several problems here:
• Using the idea of computing the intersection and then seeing whether
it is equal to a is a nice idea. But it’s hard to see that’s what this code
is doing. Much better would be to make a procedure to compute the
intersection, and then call it explicitly and compare the result to a.
• Comparing the length of the intersection and the length of a is danger-
ous. Because we allow repetitions of elements in the lists representing
sets, two sets that are equal, in the set sense, may not have the same
length.
• The break statement exits one level of the loop you are currently exe-
cuting. It is often hard to understand and best avoided.
• If you ever find yourself writing:

if condition:
return True
else:
return False

You could always replace that with

return condition

which is shorter and clearer.


Chapter 2 Learning to Program in Python 6.01— Spring 2011— February 1, 2011 38

Bad Example 2.

def isSubset(a,b):
itera = 0
okay = True
while itera < len(a):
iterb = 0
tempokay = False
while iterb < len(b):
if a[itera] == b[iterb]:
tempokay = True
iterb = iterb + 1
if tempokay == False:
okay = False
itera = itera + 1
return okay

This procedure is basically iterating over both lists, which is fine, but it is
using while rather than for to do so, which makes it hard to follow. We
can rewrite it with for:

def isSubset(a,b):
okay = True
for itera in range(len(a)):
tempokay = False
for iterb in range(len(b)):
if a[itera] == b[iterb]:
tempokay = True
if tempokay == False:
okay = False
return okay

continued
Chapter 2 Learning to Program in Python 6.01— Spring 2011— February 1, 2011 39

Bad Example 3. Previous bad example, being made over.


Now, the remaining lack of clarity is in the handling of the Booleans. We
can replace

if a[itera] == b[iterb]:
tempokay = True

with

tempokay = tempokay or (a[itera] == b[iterb])

which will set tempokay to True if a[itera] == b[iterb], and other-


wise leave it the same. And we can replace

if tempokay == False:
okay = False

with

okay = okay and tempokay

It still has the effect that if tempokay is false, then okay will be false, and
otherwise it will be as it was before. So, now we have:

def isSubset(a,b):
okay = True
for itera in range(len(a)):
tempokay = False
for iterb in range(len(b)):
tempokay = tempokay or a[itera] == b[iterb]
okay = okay and tempokay
return okay

The logical structure is becoming clearer. It might be even better if we were


to write:

def isSubset(a,b):
foundAllAsSoFar = True
for itera in range(len(a)):
foundThisA = False
for iterb in range(len(b)):
foundThisA = foundThisA or a[itera] == b[iterb]
foundAllAsSoFar = foundAllAsSoFar and foundThisA
return okay
Chapter 2 Learning to Program in Python 6.01— Spring 2011— February 1, 2011 40

Exercise 2.4. Now, see if you can, first, explain, and then improve this example!

def issubset(a,b):
i = 0
j = 0
while i < len(a):
c = False
while j < len(b):
if a[i] == b[j]:
c = True
j = j+1
if c:
c = False
else:
return False
j = 0
i = i+1
return True
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 41

Chapter 3
Programs and Data

Object-oriented programming is a popular way of organizing programs, which groups together


data with the procedures that operate on them, thus facilitating some kinds of modularity and
abstraction. In the context of our PCAP framework, object-oriented programming will give us
methods for capturing common patterns in data and the procedures that operate on that data, via
classes, generic functions, and inheritance.
In this chapter, we will try to develop a deep understanding of object-oriented programming by
working through the mechanism by which an interpreter evaluates a computer program. The first
part of the chapter will focus on interpretation of typical expxressions, starting from the simplest
single-statement programs and working up through list structures and procedures. Many of the
observations made through this process apply to styles of program organization as well as object-
oriented programming. Once we understand how an interpreter evaluates standard expressions,
we will move to objects and classes. Although we use Python as an example, the discussion in
this chapter is intended to be illustrative of principles of computer languages, more generally.
In many computer languages, including Python, programs are understood and executed by a
computer program called an interpreter. Interpreters are surprisingly simple: the rules defining
the meaning or semantics of a programming language are typically short and compact; and the in-
terpreter basically encodes these rules and applies them to any legal expressionn in the language.
The enormous richness and complexity of computer programs comes from the composition of
primitive elements with simple rules. The interpreter, in essence, defines the semantics of the
language by capturing the rules governing the value or behavior of program primitives, and of
what it means to combine the primitives in various ways. We will study the meaning of computer
programs by understanding how the interpreter operates on them.
An interpreter is made up of four pieces:
• The reader or tokenizer takes as input a string of characters and divides them into tokens,
which are numbers (like -3.42), words (like while or a), and special characters (like :).
• The parser takes as input the string of tokens and understands them as constructs in the pro-
gramming language, such as while loops, procedure definitions, or return statements.
• The evaluator (which is also sometimes called the interpreter, as well) has the really interesting
job of determining the value and effects of the program that you ask it to interpret.
• The printer takes the value returned by the evaluator and prints it out for the user to see.
Programs should never be a mystery to you: you can learn the simple semantic rules of the lan-
guage and, if necessary, simulate what the interpreter would do, in order to understand any com-
puter program which you are facing. Of course, in general, one does not want to work through
the tedious process of simulating the interpreter, but this foundation of understanding the inter-
preter’s process enables you to reason about the evaluation of any program.
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 42

3.1 Primitives, Composition, Abstraction, and Patterns


We will start by thinking about how the PCAP framework applies to computer programs, in
general. We can do this by filling in table 3.1, exploring the PCAP ideas in data, procedures, and
objects.

Data
The primitive data items in most programming languages are things like integers, floating point
numbers, and strings. We can combine these into data structures (we discuss some basic Python
data structures in section 3.3) such as lists, arrays, dictionaries and records. Making a data struc-
ture allows us, at the most basic level, to think of a collection of primitive data elements as if
it were one thing, freeing us from details. Sometimes, we just want to think of a collection of
data, not in terms of its underlying representation, but in terms of what it represents. So, we
might want to think of a set of objects, or a family tree, without worrying whether it is an array
or a list in its basic representation. Abstract data types provide a way of abstracting away from
representational details and allowing us to focus on what the data really means.

Procedures
The primitive procedures of a language are things like built-in numeric operations and basic list
operations. We can combine these using the facilities of the language, such as if and while, or
by using function composition (f(g(x))). If we want to abstract away from the details of how
a particular computation is done, we can define a new function; defining a function allows us
to use it for computational jobs without thinking about the details of how those computational
jobs get done. You can think of this process as essentially creating a new primitive, which we
can then use while ignoring the details of how it is constructed. One way to capture common
patterns of abstraction in procedures is to abstract over procedures themselves, with higher-order
procedures, which we discuss in detail in section 3.4.6.

Objects
Object-oriented programming provides a number of methods of abstraction and pattern capture
in both data and procedures. At the most basic level, objects can be used as records, combining
together primitive data elements. More generally, they provide strategies for jointly abstracting a
data representation and the procedures that work on it. The features of inheritance and polymor-
phism are particularly important, and we will discuss them in detail later in this chapter.

Procedures Data

Primitives +, *, == numbers, strings


Means of combination if, while, f(g(x)) lists, dictionaries, objects
Means of abstraction def ADTS, classes
Means of capturing patterns higher-order procedures generic functions, inheritance

Table 3.1 Primitives, combination, abstraction, patterns framework for computer programs
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 43

3.2 Expressions and assignment


We can think of most computer programs as performing some sort of transformation on data. Our
program might take as input the exam scores of everyone in the class and generate the average
score as output. Or, in a transducer model, we can think about writing the program that takes
the current memory state of the transducer and an input, and computes a new memory state and
output.
To represent data in a computer, we have to encode it, ultimately as sequences of binary digits (0s
and 1s). The memory of a computer is divided into ’words’, which typically hold 32 or 64 bits;
a word can be used to store a number, one or several characters, or a pointer to (the address of)
another memory location.
A computer program, at the lowest level, is a set of primitive instructions, also encoded into
bits and stored in the words of the computer’s memory. These instructions specify operations to
be performed on the data (and sometimes the program itself) that are stored in the computer’s
memory. In this class, we will not work at the level of these low-level instructions: a high-level
programming language such as Python lets us abstract away from these details. But it is important
to have an abstract mental model of what is going on within the computer.

3.2.1 Simple expressions


A cornerstone of a programming language is the ability to evaluate expressions. We will start
here with arithmetic expressions, just to get the idea. An expression consists of a sequence of
’tokens’ (words, special symbols, or numerals) that represent the application of operators to data
elements. Each expression has a value, which can be computed recursively by evaluating primi-
tive expressions, and then using standard rules to combine their values to get new values.
Numerals, such as 6 or -3.7 are primitive expressions, whose values are numeric constants.
Their values can be integers, within some fixed range dictated by the programming language,
or floating point numbers. Floating point numbers are used to represent non-integer values, but
they are different, in many important ways, from the real numbers. There are infinitely many
real numbers within a finite interval, but only finitely many floating-point numbers exist at all
(because they all must be representable in a fixed number of bits). In fact, the usual laws of real
arithmetic (transitivity, associativity, etc.) are violated in floating-point arithmetic, because the
results of any given sub-computation may not be representable in the given number of bits.
We will illustrate the evaluation of expressions in Python by showing short transcripts of interac-
tive sessions with the Python shell : the shell is a computer program that
• Prompts the user for an expression, by typing >>>,
• Reads what the user types in, and converts it into a set of tokens,
• Parses the tokens into a data structure representing the syntax of the expression,
• Evaluates the parsed expression using an interpreter, and
• Prints out the resulting value

So, for example, we might have this interaction with Python:

>>> 2 + 3
5
>>> (3 * 8) - 2
22
>>> ((3 * 8) - 2) / 11
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 44

2
>>> 2.0
2.0
>>> 0.1
0.10000000000000001
>>> 1.0 / 3.0
0.33333333333333331
>>> 1 / 3
0

There are a couple of things to observe here. First, we can see how floating point numbers only
approximately represent real numbers: when we type in 0.1, the closest Python can come to it in
floating point is 0.10000000000000001. The last interaction is particularly troubling: it seems
like the value of the expression 1 / 3 should be something like 0.33333. However, in Python, if
both operands to the / operator are integers, then it will perform an integer division, truncating
any remainder. 12
These expressions can be arbitrarily deeply nested combinations of primitives. The rules used for
evaluation are essentially the same as the ones you learned in school; the interpreter proceeds by
applying the operations in precedence order 13, evaluating sub-expressions to get new values, and
then evaluating the expressions those values participate in, until a single value results.

3.2.2 Variables
We cannot go very far without variables. A variable is a name that we can bind to have a partic-
ular value and then later use in an expression. When a variable is encountered in an expression,
it is evaluated by looking to see to what value it is bound.
An interpreter keeps track of which variables are bound to what values in binding environments.
An environment specifies a mapping between variable names and values. The values can be inte-
gers, floating-point numbers, characters, or pointers to more complex entities such as procedures
or larger collections of data.
Here is an example binding environment:

b 3
x 2.2
foo -1012

Each row represents a binding: the entry in the first column is the variable name and the entry in
the second column is the value it to which it is bound.
When you start up the Python shell, you immediately start interacting with a local binding en-
vironment. You can add a binding or change an existing binding by evaluating an assignment
statement of the form:

<var> = <expr>

12
This behavior will no longer be the default in Python 3.0.
13
Please Excuse My Dear Aunt Sally (Parentheses, Exponentiation, Multiplication, Division, Addition, Subtraction)
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 45

where <var> is a variable name (a string of letters or digits or the character _, not starting with a
digit) and <expr> is a Python expression. 14
Expressions are always evaluated in some environment.
We might have the following interaction in a fresh Python shell:

>>> a = 3
>>> a
3
>>> b
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name ’b’ is not defined
>>>

We started by assigning the variable a to have the value 3. That added a binding for a to the local
environment.
Next, we evaluated the expression a. The value of an expression with one or more variable names
in it cannot be determined unless we know with respect to what environment it is being evaluated.
Thus, we will always speak of evaluating expressions in an environment. During the process of
evaluating an expression in some environment E, if the interpreter comes to a variable, it looks up
that variable in E: if E contains a binding for the variable, then the associated value is returned;
if it does not, then an error is generated. In the Python shell interaction above, we can see that
the interpreter was able to find a binding for a and return a value, but it was not able to find a
binding for b.
Why do we bother defining values for variables? They allow us to re-use an intermediate value
in a computation. We might want to compute a formula in two steps, as in:

>>> c = 952**4
>>> c**2 + c / 2.0
6.7467650588636822e+23

They will also play a crucial role in abstraction and the definition of procedures. By giving a
name to a value, we can isolate the use of that value in other computations, so that if we decide
to change the value, we only have to change the definition (and not change a value several places
in the code).
It is fine to reassign the value of a variable; although we use the equality symbol = to stand for
assignment, we are not making a mathematical statement of equality. So, for example, we can
write:

>>> a = 3
>>> a = a + 1
>>> a
4

14
When we want to talk about the abstract form or syntax of a programming language construct, we will often use meta-
variables, written with angle brackets, like <var>. This is meant to signify that <var> could be any Python variable
name, for example.
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 46

Exercise 3.1. What is the result of evaluating this sequence of assignment statements
and the last expression? Determine this by hand-simulating the Python
interpreter. Draw an environment and update the stored values as you
work through this example.

>>> a = 3
>>> b = a
>>> a = 4
>>> b

3.3 Structured data


We will often want to work with large collections of data. Rather than giving each number its
own name, we want to organize the data into natural structures: grocery lists, matrices, sets of
employee medical records. In this section, we will explore a simple but enormously useful and
flexible data structure, which is conveniently built into Python: the list. The precise details of how
lists are represented inside a computer vary from language to language. We will adopt an abstract
model in which we think of a list as an ordered sequence of memory locations that contain values.
So, for example, in Python, we can express a list of three integers as:

>>> [1, 7, -2]


[1, 7, -2]

which we will draw in an abstract memory diagram as:

1 7 -2

We can assign a list to a variable:

>>> a = [2, 4, 9]

A binding environment associates a name with a single fixed-size data item. So, if we want to
associate a name with a complex structure, we associate the name directly with a ’pointer’ to
(actually, the memory address of) the structure. So we can think of a as being bound to a ’pointer’
to the list:

a 2 4 9

Now that we have lists, we have some new kinds of expressions, which let us extract components
of a list by specifying their indices. An index of 0 corresponds to the first element of a list. An
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 47

index of -1 corresponds to the last element (no matter how many elements there are). 15 So, if a is
bound as above, then we would have:

>>> a[0]
2
>>> a[2]
9
>>> a[-1]
9
>>> a[3]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list index out of range

Note that if we attempt to access an element of the list that is not present (in this case, the fourth
element of a three-element list), then an error is generated.
Lists can be nested inside one another. The Python expression:

>>> c = [3, [1], [2, 1], [[4]]]

creates a list that looks, in memory, like this:

c 3

1 2 1 4

It is also possible to have an empty list, which is written in Python as []. We will draw it in our
memory diagrams as a small black box. So, for example, this list

>>> z = [3, [], [[]]]

looks like this in memory:

z 3

Python has a useful function, len, which takes a list as an argument and returns its length. It does
not look inside the elements of the list—it just returns the number of elements at the top level of
structure. So, we have

15
See the Python tutorial for much more on list indexing.
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 48

>>> len([1, 2, 3])


3
>>> len([[1, 2, 3]])
1

Exercise 3.2. Draw a diagram of the binding environment and memory structure after
the following statement has been evaluated:

a = [[1], 2, [3, 4]]

Exercise 3.3. Draw a diagram of the binding environment and memory structure after
the following statement has been evaluated:

a = [[[]]]

Exercise 3.4. Give a Python statement which, when evaluated, would give rise to this
memory structure:

c -2

1 2

4 -5

What is the value, in this environment, of the following expressions:


• c[1]
• c[-1]
• c[2][1]

3.3.1 List mutation and shared structure


Lists are mutable data structures, which means that we can actually change the values stored in
their elements. We do this by using element-selection expressions, like a[1] on the left-hand side
of an assignment statement. So, the assignment

a[1] = -3

assigns the second element of a to be -3. In more detail, the left-hand side of this expression
evaluates to a pointer to a specific location in memory (just as a’s value is a pointer to a location
in memory); then the assignment statement changes the value stored there by inserting the value
of the right-hand side of the expression. If that statement were evaluated in this environment,
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 49

a 2 4 9

then the resulting environment would be:

a 2 -3 9

We have permanently changed the list named a.


In this section, we will explore the consequences of the mutability of lists; programs that change
list structure can become very confusing, but you can always work your way through what is
happening by drawing out the memory diagrams.
Continuing the previous example, let us remember that a is bound directly to a pointer to a list
(or a sequence of memory cells), and think about what happens if we do:

>>> b = a

Now, a and b are both names for the same list structure, resulting in a memory diagram like this:

a 2 -3 9
b

Now, we can reference parts of the list through b, and even change the list structure that way:

>>> b[0]
2
>>> b[2] = 1

Notice that, because a and b point to the same list, changing b changes a!

>>> a
[2, -3, 1]

Here is the memory picture now:

a 2 -3 1
b

This situation is called aliasing: the name b has become an alias for a. Aliasing can be useful,
but it can also cause problems, because you might inadvertently change b (by passing it into a
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 50

procedure that changes one of its structured arguments, for example) when it is important to you
to keep a unmodified.
Another important way to change a list is to add or delete elements. We will demonstrate adding
elements to the end of a list, but see the Python documentation for more operations on lists. This
statement

>>> a.append(9)

causes a new element to be added to the end of the list name a. The resulting memory state is:

a 2 -3 1 9
b

As before, because a and b are names for the same list (i.e., they point to the same memory
sequence), b is changed too. This is a side effect of the aliasing between a and b:

>>> b
[2, -3, 1, 9]

Often, it will be important to make a fresh copy of a list so that you can change it without affecting
the original one. Here are two equivalent ways to make a copy (use whichever one you can
remember):

>>> c = list(a)
>>> c = a[:]

Here is a picture of the memory at this point:

a 2 -3 1 9
b
c 2 -3 1 9

Now, if we change an element of c, it does not affect a (or b!):

>>> c[0] = 100


>>> c
[100, -3, 1, 9]
>>> a
[2, -3, 1, 9]

We can make crazy lists that share structure within a single list:

>>> f = [1, 2, 3]
>>> g = [1, f, [f]]
>>> g
[1, [1, 2, 3], [[1, 2, 3]]]

which results in this memory structure:


Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 51

f 1 2 3
g

If you want to add an element to a list and get a new copy at the same time, you can do

>>> a + [1]

The + operator makes a new list that contains the elements of both of its arguments, but does not
share any top-level structure. All of our methods of copying only work reliably if your lists do
not contain other lists, because it only copies one level of list structure. So, for example, if we did:

>>> h = list(g)

we would end up with this picture:

f 1 2 3
g
h
1

It is clear that if we were to change f, it would change h, so this is not a completely new copy. If
you need to copy deep structures, that is, to make a copy not only of the top level list structure,
but of the structures of any lists that list contains, and the lists those lists contain, etc., you will
need to use the Python copy.deepcopy procedure.

Exercise 3.5. Give a sequence of Python statements which, when evaluated, would give
rise to this memory structure:

a 1 2 3
b
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 52

Exercise 3.6. Give a sequence of Python statements which, when evaluated, would give
rise to this memory structure:

a 3
b
1 2

Exercise 3.7. Show the memory structure after this sequence of expressions.

>>> a = [1, 2, 3]
>>> b = [a, a]
>>> a.append(100)

What will be the value of b at this point?

Exercise 3.8. Show the memory structure after this sequence of expressions.

>>> a = [5, 6]
>>> b = [1, 2]
>>> c = b + a

We will use this “curvy road” symbol to indicate sections of the notes or exercises that are some-
what more difficult and not crucial to understanding the rest of the notes. Feel free to skip them
on first reading; but, of course, we think they are cool. 16

Exercise 3.9.
Show the memory structure after this sequence of expressions.

>>> a = [1, 2, 3]
>>> a[1] = a

What will be the value of a at this point?

3.3.2 Tuples and strings


Python has two more list-like data types that are important to understand.
A tuple is a structure that is like a list, but is not mutable. You can make new tuples, but you
cannot change the contents of a tuple or add elements to it. A tuple is typically written like a list,
but with round parentheses instead of square ones:

16
Thanks to Don Knuth’s Art of Computer Programming for the idea of the curvy road sign.
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 53

>>> a = (1, 2, 3)

In fact, it is the commas and not the parentheses that matter here. So, you can write

>>> a = 1, 2, 3
>>> a
(1, 2, 3)

and still get a tuple. The only tricky thing about tuples is making a tuple with a single element.
We could try

>>> a = (1)
>>> a
1

but it does not work, because in the expression (1) the parentheses are playing the standard
grouping role (and, in fact, because parentheses do not make tuples). So, to make a tuple with a
single element, we have to use a comma:

>>> a = 1,
>>> a
(1,)

This is a little inelegant, but so it goes.


Tuples will be important in contexts where we are using structured objects as ’keys’, that is, to
index into another data structure, and where inconsistencies would occur if those keys could be
changed.
An important special kind of tuple is a string. A string can almost be thought of as a tuple of
characters. The details of what constitutes a character and how they are encoded is complicated,
because modern character sets include characters from nearly all the world’s languages. We will
stick to the characters we can type easily on our keyboards. In Python, you can write a string with
either single or double quotes: ’abc’ or "abc". You can select parts of it as you would a list:

>>> s = ’abc’
>>> s[0]
’a’
>>> s[-1]
’c’

The strange thing about this is that s is a string, and because Python has no special data type to
represent a single character, s[0] is also a string.
We will frequently use + to concatenate two existing strings to make a new one:

>>> to = ’Jody’
>>> fromP = ’Robin’
>>> letter = ’Dear ’ + to + ",\n It’s over.\n" + fromP
>>> print letter
Dear Jody,
It’s over.
Robin

As well as using + to concatenate strings, this example illustrates several other small but impor-
tant points:
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 54

• You can put a single quote inside a string that is delimited by double-quote characters (and vice
versa).
• If you want a new line in your string, you can write \n. Or, if you delimit your string with a
triple quote, it can go over multiple lines.
• The print statement can be used to print out results in your program.
• Python, like most other programming languages, has some reserved words that have special
meaning and cannot be used as variables. In this case, we wanted to use from, but that has a
special meaning to Python, so we used fromP instead.

Structured assignment
Once we have lists and tuples, we can use a nice trick in assignment statements, based on the
packing and unpacking of tuples.

>>> a, b, c = 1, 2, 3
>>> a
1
>>> b
2
>>> c
3

Or, with lists,

>>> [a, b, c] = [1, 2, 3]


>>> a
1
>>> b
2
>>> c
3

When you have a list (or a tuple) on the left-hand side of an assignment statement, you have to
have a list (or tuple) of matching structure on the right-hand side. Then Python will “unpack”
them both, and assign to the individual components of the structure on the left hand side. You
can get fancier with this method:

>>> thing = [8, 9, [1, 2], ’John’, [33.3, 44.4]]


>>> [a, b, c, d, [e1, e2]] = thing
>>> c
[1, 2]
>>> e1
33.299999999999997

3.4 Procedures
Procedures are computer-program constructs that let us capture common patterns of computation
by:
• Gathering together sequences of statements
• Abstracting away from particular data items on which they operate.
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 55

Here is a procedure definition, 17 and then its use:

def square(x):
return x * x

>>> square(6)
36
>>> square(2 - square(2))
4

We will work through, in detail, what happens when the interpreter evaluates a procedure defin-
ition, and then the application of that procedure.

3.4.1 Definition
A procedure definition has the abstract form:

def <name>(<fp1>, ..., <fpn>):


<statement1>
...
<statementk>

There are essentially three parts:


• <name> is a name for the procedure, with the same restrictions as a variable name;
• <fp1>, ..., <fpn> is a list of formal parameters, which will stand for the data items on
which this procedure will operate; and
• <statement1>, ..., <statementk>, known as the body of the procedure, is a list of Python
statements (right now, we know about assignment statements, print statements, and basic ex-
pressions, but there will be more.)

When we evaluate a procedure definition in an environment, 18 E, Python does two things:


1. Makes a procedure object 19 that contains the formal parameters, the body of the procedure, and
a pointer to E; and then
2. Binds the name to have this procedure as its value.

Here is an example of an environment after we have evaluated

def square(x):
return x * x

E1
a 2 Procedure1
square (x)
return x*x

17
In the code displayed in these notes, we will show procedures being defined and then used, as if the definitions were
happening in the Python shell (but without the prompts). In fact, you should not type procedure definitions into the
shell, because if you make a mistake, you will have to re-type the whole thing, and because multi-line objects are not
handled very well. Instead, type your procedure definitions into a file in Idle, and then test them by ’running’ the file in
Idle (which will actually evaluate all of the expressions in your file) and then evaluating test expressions in Idle’s shell.
18
Remember that every expression is always evaluated with respect to some environment.
19
In our memory diagrams, we will show the procedure object as a box with the word Procedure<N>, where N is some
integer, at the top; we give the procedure objects these numbers so we can refer to them easily in the text.
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 56

Note how the construct to which square points is a procedure object, with a list of formal para-
meters, a body, and a pointer to its environment.

3.4.2 Procedure calls


When you evaluate an expression of the form

<expr0>(<expr1>, ..., <exprn>)

the Python interpreter treats this as a procedure call. It will be easier to talk about a specific case
of calling a procedure, so we will illustrate with the example

>>> square(a + 3)

evaluated in environment E1. Here are the steps:


1. The expression that determines the procedure (<expr0>) is evaluated. In this case, we evaluate
square in E1 and get Procedure1.
2. The expressions that determine the arguments (<expr1>, ..., <exprn>) are evaluated. In
this case, we evaluate a + 3 in E1 and get 5.
3. A new environment (in this case E2) is created, which:
− binds the formal parameters of the procedure (in this case x) to the values of its arguments
(in this case, 5); and
− has as its parent the environment in which the procedure was defined (we find a pointer
to this environment stored in the procedure; in this case E1 is its parent ).
At this point, our memory looks like this:

E1
a 2 Procedure1
square (x)
return x*x

E2 x 5

The dotted line between E2 and E1 is intended to indicate that E1 is the parent environment
of E2.
4. The statements of the procedure body are evaluated in the new environment until either a
return statement or the end of the list of statements is reached. If a return statement is eval-
uated, the expression after the return is evaluated and its value is returned as the value of the
procedure-call expression. Otherwise, the procedure has no return value, and the expression
has the special Python value None.
In our example, the only statement in the body is a return statement. So, we evaluate the
expression x * x in E2, obtaining a value of 25. That value is returned as the value of the
entire procedure-call expression square(a + 3).
This basic mechanism can generate behavior of arbitrary complexity, when coupled with recur-
sion or other control structures, discussed in section 2.3.
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 57

Worked example 1
Let’s examine what happens when we evaluate the following Python code:

def p(x, y):


z = x*x - y
return z + 1/z

>>> p(1.0, 2.0)


-2.0

Here is a picture of the calling environment (E1) and the procedure-call environment (E2) just
before the body of the procedure is evaluated:

E1
Procedure2
p (x, y)
z = x*x-y
return z

E2 x 1.0
y 2.0

After evaluating z = x*x - y in E2, we have:

E1 Procedure2
(x, y)
p z = x*x-y
return z+1/z

E2 x 1.0
y 2.0
z -1.0

Finally, we evaluate z + 1/z in E2 and get -2.0, which we return.

Worked example 2
Here is another example:

def silly(a):
a = a + 1
return a

>>> b = 6
>>> silly(b)
7

Here is a picture of the calling environment (E1) and the procedure-call environment (E2) just
before the body of the procedure is evaluated:
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 58

E1 Procedure3
(a)
silly a=a+1
b 6 return a

E2 a 6

After evaluating a = a + 1 in E2, we have:

E1 Procedure3
(a)
silly a=a+1
b 6 return a

E2 a 7

Finally, we evaluate a in E2 and get 7, which we return.


Convince yourself that the execution below works fine, and notice that having a binding for a in
E2 means that we leave the binding for a in E1 unmolested:

>>> a = 2
>>> silly(a)
3
>>> a
2

Exercise 3.10. Draw the environment diagram at the time the statement with the arrow
is being executed:

def fizz(x, y):


p = x + y
q = p*p
return q + x # <-----------

>>> fizz(2, 3)

Exercise 3.11. Draw the environment diagram at the time the statement with the arrow
is being executed:

def fuzz(a, b):


return a + b # <----------

def buzz(a):
return fuzz(a, square(a))

>>> buzz(2)
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 59

3.4.3 Non-local references


So far, whenever we needed to evaluate a variable, there was a binding for that variable in the
’local’ environment (the environment in which we were evaluating the expression). But consider
this example:

def biz(a):
return a + b

>>> b = 6
>>> biz(2)

This actually works fine, and returns 8. Let’s see why. Here is the environment, E1, in which the
Python shell is working, after we execute the b = 6 statement:

E1
Procedure4
biz (a)
return a+b
b 6

Now, when we evaluate biz(2) in E1, we make a new environment, E2, which binds a to 2 and
points to E1 as its parent environment.

E1
Procedure4
biz (a)
return a+b
b 6

E2 a 2

We need to elaborate, slightly, how it is that a variable v is evaluated in an environment E:


• We look to see if there is a binding for v in E; if so, we stop and return it.
• If not, we evaluate v in the parent environment of E. If E has no parent, we generate an error.

It is important to appreciate that this process will continue up an arbitrarily long chain of environ-
ments and their parents until either a binding for v is found or there are no more environments to
examine.
So, in our case, we will evaluate the expression a + b in environment E2. We start by evaluating
a and finding value 2 in E2. Then, we evaluate b and cannot find it in E2...but we don’t panic! We
follow the parent pointer to E1 and try again. We find a binding for b in E1 and get the value 6.
So, the value of a + b in E2 is 8.
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 60

Exercise 3.12. Draw a picture of the relevant binding environments when the statement
with the arrow is being executed. What value does the procedure return?

def f(a):
return a + g(a)

def g(b):
return a + b # <-------------

>>> f(3)

Exercise 3.13. Draw a picture of the relevant binding environments when the statement
with the arrow is being executed. What value does the procedure return?

def f(a):
def g(b):
return a + b # <-------------
return a + g(a)

>>> f(3)

Exercise 3.14. Draw a picture of the relevant binding environments when the statement
with the arrow is being executed. What value does the procedure return?
Does it cause an error?

def a(a):
return a * a

>>> a(2)

3.4.4 Environments in Python


Generally, Python establishes the following binding environments:
1. __builtin__: the mother of all environments: it contains the definitions of all sorts of basic
symbols, like list and sum. It is the parent of all module environments.
2. Module: each separate file that contains Python code is called a module and establishes its
own environment, whose parent is __builtin__.
3. Procedure calls: as described in this section. A procedure that is defined at the ’top level’ of a
module (that is, not nested in the definition of another procedure) has the module’s environ-
ment as its parent, and has its name defined in the module’s environment. Procedures that
are defined inside other procedures have the procedure-call environment of the containing
procedure as their parent.
We have seen two operations that cause bindings to be created: assignments and procedure calls.
Bindings are also created when you evaluate an import statement. If you evaluate
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 61

import math

then a file associated with the math module is evaluated and the name math is bound, in the
current environment, to the math module, which is an environment. No other names are added
to the current environment, and if you want to refer to names in that module, you have to qualify
them, as in math.sqrt. (As we will see in a subsequent section, the evaluation of this expression
first evaluates math, which returns an environment. We can then evaluate sqrt with respect to
that environment, thus returning a pointer to the procedure stored there.) If you execute

from math import sqrt

then the math file is evaluated, and the name sqrt is bound, in the current environment, to
whatever the name sqrt is bound in the math module. But note that if you do this, the name
math is not bound to anything, and you cannot access any other procedures in the math module
unless you import them explicitly, as well.

3.4.5 Non-local references in procedures


There is an important subtlety in the way names are handled in the environment created by a
procedure call. When a name that is not bound in the local environment is referenced, then it is
looked up in the chain of parent environments. So, as we have seen, it is fine to have

a = 2
def b():
return a

When a name is assigned inside a procedure, a new binding is created for it in the environment
associated with the current call of that procedure. So, it is fine to have

a = 2
def b():
a = 3
c = 4
return a + c

Both assignments cause new bindings to be made in the local environment, and it is those bind-
ings that are used to supply values in the return expression. It will not change a in the global
environment.
But here is a code fragment that causes trouble:

a = 3
def b():
a = a + 1
print a

It seems completely reasonable, and you might expect b() to return 4. But, instead, it generates
an error. What is going on?? It all has to do with when Python decides to add a binding to the
local environment. When it sees this procedure definition, it sees that the name a occurs on the
left-hand-side of an assignment statement, and so, at the very beginning, it puts a new entry for a
in the local environment, but without any value bound to it. Now, when it is time to evaluate the
statement
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 62

a = a + 1

Python starts by evaluating the expression on the right hand side: a + 1. When it tries to look up
the name a in the procedure-call environment, it finds that a has been added to the environment,
but has not yet had a value specified. So it generates an error.
In Python, we can write code to increment a number named in the global environment, by using
the global declaration:

a = 3
def b():
global a
a = a + 1
print a
>>> b()
4
>>> b()
5
>>> a
5

The statement global a asks that a new binding for a not be made in the procedure-call envi-
ronment. Now, all references to a are to the binding in the module’s environment, and so this
procedure actually changes a.
In Python, we can only make assignments to names in the procedure-call environment or to the
module environment, but not to names in intermediate environments. So, for example,

def outer():
def inner():
a = a + 1
a = 0
inner()

In this example, we get an error, because Python has made a new binding for a in the environment
for the call to inner. We would really like inner to be able to see and modify the a that belongs
to the environment for outer, but there is no way to arrange this. Some other programming
languages, such as Scheme, offer more fine-grained control over how the scoping of variables is
handled.

3.4.6 Procedures as first-class objects


In Python, unlike many other languages, procedures are treated in much the same way as num-
bers: they can be stored as values of variables, can be passed as arguments to procedures, and
can be returned as results of procedure calls. Because of this, we say that procedures are treated
as “first-class” objects. We will explore this treatment of “higher-order” procedures (procedures
that manipulated procedures) throughout this section.
First of all, it is useful to see that we can construct (some) procedures without naming them using
the lambda constructor:

lambda <var1>, ..., <varn> : <expr>

The formal parameters are <var1>, ..., <varn> and the body is <expr>. There is no need for
an explicit return statement; the value of the expression is always returned. A single expression
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 63

can only be one line of Python, such as you could put on the right hand side of an assignment
statement. Here are some examples:

>>> f = lambda x: x*x


>>> f
<function <lambda> at 0x4ecf0>
>>> f(4)
16

Here is a procedure of two arguments defined with lambda:

>>> g = lambda x,y : x * y


>>> g(3, 4)
12

Using the expression-evaluation rules we have already established, we can do some fancy things
with procedures, which we will illustrate throughout the rest of this section.

>>> procs = [lambda x: x, lambda x: x + 1, lambda x: x + 2]


>>> procs[0]
<function <lambda> at 0x83d70>
>>> procs[1](6)
7

Here, we have defined a list of three procedures. We can see that an individual element of the list
(e.g., procs[0]) is a procedure. 20 So, then procs[1](6) applies the second procedure in the list
to the argument 6. Since the second procedure returns its argument plus 1, the result is 7.
Here is a demonstration that procedures can be assigned to names in just the way other values
can.

>>> fuzz = procs[2]


>>> fuzz(3)
5

def thing(a):
return a * a

>>> thing(3)
9
>>> thang = thing
>>> thang(3)
9

Passing procedures as arguments


Just as we can pass numbers or lists into procedures as arguments, we can pass in procedures as
arguments, as well.
What if we find that we are often wanting to perform the same procedure twice on an argument?
That is, we seem to keep writing square(square(x)). If it were always the same procedure we
were applying twice, we could just write a new procedure

20
In the programming world, people often use the words “function” and “procedure” either interchangeable or with
minor subtle distinctions. The Python interpreter refers to the objects we are calling procedures as “functions.”
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 64

def squaretwice(x):
return square(square(x))

But what if it is different procedures? We can write a new procedure that takes a procedure f as
an argument and applies it twice:

def doTwice(f, x):


return f(f(x))

So, if we wanted to square twice, we could do:

>>> doTwice(square,2)
16

Here is a picture of the environment structure in place when we are evaluating the return
f(f(x)) statement in doTwice:

Procedure5
(x)
return x * x
E1
square
doTwice
Procedure6
(f, x)
return f(f(x))
f
E2 x 2

Environment E1 is the module environment, where procedures square and doTwice were de-
fined and where the expression doTwice(square, 2) is being evaluated. The interpreter:
• Evaluates doTwice in E1 and gets Procedure6.
• Evaluates square in E1 and gets Procedure5.
• Evaluates 2 in E1 and gets 2.
• Makes the new binding environment E2, binding the formal parameters, f and x, of Procedure
6, to actual arguments Procedure5 and 2.
• Evaluates the body of Procedure6 in E2.

Now, let’s peek one level deeper into the process. To evaluate f(f(x)) in E2, the interpreter starts
by evaluating the inner f(x) expression. It
• Evaluates f in E2 and gets Procedure5.
• Evaluates x in E2 and gets 2.
• Makes the new binding environment E3, binding the formal parameter x, of Procedure5, to 2.
(Remember that the parent of E3 is E1 because Procedure5 has E1 as a parent.)
• Evaluates the body of Procedure5 in E3, getting 4.

The environments at the time of this last evaluation step are:


Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 65

Procedure5
(x)
return x * x
E1
square
doTwice
Procedure6
(f, x)
return f(f(x))
f
E2 x 2

E3 x 2

A similar thing happens when we evaluate the outer application of f, but now with argument 4,
and a return value of 16.

Exercise 3.15. Here is the definition of a procedure sumOfProcs that takes two proce-
dures, f and g, as well as another value x, as arguments, and returns f(x)
+ g(x). The sumOfProcs procedure is then applied to two little test pro-
cedures:

def sumOfProcs(f, g, x):


return f(x) + g(x)

def thing1(a):
return a*a*a
def thing2(b):
return b+1 # <--------------------

>>> sumOfProcs(thing1, thing2, 2)

Draw a picture of all of the relevant environments at the moment the state-
ment with the arrow is being evaluated. What is the return value of the
call to sumOfProcs?

Returning procedures as values


Another way to apply a procedure multiple times is this:

def doTwiceMaker(f):
return lambda x: f(f(x))

This is a procedure that returns a procedure! If you would rather not use lambda, you could write
it this way:

def doTwiceMaker(f):
def twoF(x):
return f(f(x))
return twoF
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 66

Now, to use doTwiceMaker, we might start by calling it with a procedure, such as square, as an
argument and naming the resulting procedure.

>>> twoSquare = doTwiceMaker(square)

Here is a picture of the environments just before the return twoF statement in doTwiceMaker
is evaluated.

Procedure5
(x)
return x * x
E1
square
doTwiceMaker
Procedure7
(f)
def twoF(x):
return f(f(x))
f return twoF
E2 twoF

Procedure8
(x)
return f(f(x))

Here is a picture of the environments after the doTwiceMaker returns its value and it is assigned
to twoSquare in E1. It is important to see that Procedure8 is the return value of the call to
doTwiceMaker and that, because Procedure8 retains a pointer to the environment in which it
was defined, we need to keep E2 around. And it is E2 that remembers which procedure (via its
binding for f) is going to be applied twice.

Procedure5
(x)
return x * x
E1
square
doTwiceMaker
Procedure7
twoSquare (f)
def twoF(x):
return f(f(x))
return twoF
f
E2 twoF
Procedure8
(x)
return f(f(x))

Now, when we evaluate this expression in E1

>>> twoSquare(2)

we start by making a new binding environment, E3, for the procedure call. Note that, because the
procedure we are calling, Procedure8, has E2 stored in it, we set the parent of E3 to be E2.
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 67

Procedure5
(x)
return x * x
E1
square
doTwiceMaker
Procedure7
twoSquare (f)
def twoF(x):
return f(f(x))
return twoF
f
E2 twoF
Procedure8
(x)
return f(f(x))
E3 x 2

Next, we evaluate the body of Procedure8, which is return f(f(x)) in E3. Let’s just consider
evaluating the inner expression f(x) in E3. We evaluate f in E3 and get Procedure5, and eval-
uate x and get 2. Now, we make a new binding environment, E4, to bind the formal parameter
of Procedure5 to 2. Because Procedure5 has a stored pointer to E1, E4’s parent is E1, as shown
here:

Procedure5
(x)
return x * x
E1
square
doTwiceMaker
Procedure7
twoSquare (f)
def twoF(x):
return f(f(x))
return twoF
f
E2 twoF
Procedure8
(x)
return f(f(x))
E3 x 2

E4 x 2

Evaluating the body of Procedure5 in E4 yields 4. We will repeat this process to evaluate the
outer application of f, in f(f(x)), now with argument 4, and end with result 16.
Essentially the same process would happen when we evaluate

>>> doTwiceMaker(square)(2)
16

except the procedure that is created by the expression doTwiceMaker(square) is not assigned a
name; it is simply used as an intermediate result in the expression evaluation.

3.5 Object-oriented programming


We have seen structured data and interesting procedures that can operate on that data. It will of-
ten be useful to make a close association between collections of data and the operations that apply
to them. The style of programming that takes this point of view is object-oriented programming
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 68

(OOP). It requires adding some simple mechanisms to our interpreter, but is not a big conceptual
departure from the things we have already seen. It is, however, a different style of organizing
large programs.

3.5.1 Classes and instances


In OOP, we introduce the idea of classes and instances. An instance is a collection of data that
describe a single entity in our domain of interest, such as a person or a car or a point in 3D space.
If we have many instances that share some data values, or upon which we would want to perform
similar operations, then we can represent them as being members of a class and store the shared
information once with the class, rather than replicating it in the instances.
Consider the following staff database for a large undergraduate course:

name role age building room course


Pat Prof 60 34 501 6.01
Kelly TA 31 34 501 6.01
Lynn TA 29 34 501 6.01
Dana LA 19 34 501 6.01
Chris LA 20 34 501 6.01

There are lots of shared values here, so we might want to define a class. A class definition has the
form:

class <name>:
<statement1>
...
<statementn>

Here is the definition of simple class in our example domain:

class Staff601:
course = ’6.01’
building = 34
room = 501

From the implementation perspective, the most important thing to know is that classes and in-
stances are environments.
When we define a new class, we make a new environment. In this case, the act of defining class
Staff601 in an environment E1 results in a binding from Staff601 to E2, an empty environment
whose parent is E1, the environment in which the class definition was evaluated. Now, the state-
ments inside the class definition are evaluated in the new environment, resulting in a memory
state like this: 21

21
In fact, the string ’6.01’ should be shown as an external memory structure, with a pointer stored in the binding
environment; for compactness in the following diagrams we will sometimes show the strings themselves as if they
were stored directly in the environment.
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 69

E1 E2
course '6.01'
Staff601
building 34
room 501

Note how the common values and names have been captured in a separate environment.
Caveat: when we discuss methods in section 3.5.2, we will see that the rules for evaluating proce-
dure definitions inside a class definition are slightly different from those for evaluating procedure
definitions that are not embedded in a class definition.
We will often call names that are bound in a class’s environment attributes of the class. We can
access these attributes of the class after we have defined them, using the dot notation:

<envExpr>.<var>

When the interpreter evaluates such an expression, it first evaluates <envExpr>; if the result is
not an environment, then an error is signaled. If it is an environment, E, then the name <var> is
looked up in E (using the general process of looking in the parent environment if it is not found
directly in E) and the associated value returned.
So, for example, we could do:

>>> Staff601.room
501

We can also use the dot notation on the left-hand side of an assignment statement, if we wish to
modify an environment. An assignment statement of the form

<envExpr>.<var> = <valExpr>

causes the name <var> in the environment that is the result of evaluating <envExpr> to be bound
to the result of evaluating <valExpr>.
So, we might change the room in which 6.01 meets with:

Staff601.room = Staff601.room - 100

or add a new attribute with

Staff601.coolness = 11 # out of 10, of course...

Now, we can make an instance of a class with an expression of the form:

<classExpr>()

This expression has as its value a new empty environment whose parent pointer is the environ-
ment obtained as a result of evaluating <classExpr>. 22
So, if we do:

>>> pat = Staff601()

we will end up with an environment state like this:

22
Another way of thinking of this is that whenever you define a class, Python defines a procedure with the same name,
which is used to create a instance of that class.
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 70

E1 E2
course '6.01'
Staff601
building 34
pat
room 501

E3

At this point, given our standard rules of evaluation and the dot notation, we can say:

>>> pat.course
’6.01’

The interpreter evaluates pat in E1 to get the environment E3 and then looks up the name course.
It does not find it in E3, so it follows the parent pointer to E2, and finds there that it is bound to
’6.01’.
Similarly, we can set attribute values in the instance. So, if we were to do:

pat.name = ’Pat’
pat.age = 60
pat.role = ’Professor’

we would get this environment structure.

E1 E2
course '6.01'
Staff601
building 34
pat
room 501

E3
name 'Pat'
age 60
role 'Professor'

Note that these names are bound in the instance environment, not the class.
These structures are quite flexible. If we wanted to say that Professor Pat is an exception, holding
office hours in a different place from the rest of the 6.01 staff, we could say:

pat.building = 32
pat.office = ’G492’

Here is the new environment state. Nothing is changed in the Staff601 class: these assignments
just make new bindings in pat’s environment, which ’shadow’ the bindings in the class, so that
when we ask for pat’s building, we get 32.
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 71

E1 E2
course '6.01'
Staff601
building 34
pat
room 501

E3
name 'Pat'
age 60
role 'Professor'
building 32
room 'G492'

3.5.2 Methods
Objects and classes are a good way to organize procedures, as well as data. When we define a
procedure that is associated with a particular class, we call it a method of that class. Method
definition requires only a small variation on our existing evaluation rules.
So, imagine we want to be able to greet 6.01 staff members appropriately on the staff web site. We
might add the definition of a salutation method:

class Staff601:
course = ’6.01’
building = 34
room = 501

def salutation(self):
return self.role + ’ ’ + self.name

This procedure definition, made inside the class definition, is evaluated in almost the standard
way, resulting in a binding of the name salutation in the class environment to the new proce-
dure. The way in which this process deviates from the standard procedure evaluation process
is that the environment stored in the procedure is the module (file) environment, no matter how
deeply nested the class and method definitions are:
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 72

Procedure9
(self)
return self.role + ' ' \
+ self.name

E1 E2 salutation
course '6.01'
Staff601
building 34
pat
room 501

E3
name 'Pat'
age 60
role 'Professor'
building 32
room 'G492'

Now, for example, we could do:


Staff601.saluation(pat)

The interpreter finds that Staff601 is an environment, in which it looks up the name saluation
and finds Procedure9. To call that procedure we follow the same steps as in section 3.4.2 :
• Evaluate pat to get the instance E3.
• Make a new environment, E4, binding self to E3. The parent of E4 is E1, because we are
evaluating this procedure call in E1.
• Evaluate self.role + ’ ’ + self.name in E4.
• In E4, we look up self and get E3, look up role in E3 and get ’Professor’, etc.
• Ultimately, we return ’Professor Pat’.
Here is a picture of the binding environments while we are evaluating self.role + ’ ’ +
self.name.

Procedure9
(self)
return self.role + ' ' \
+ self.name

E1 E2 salutation
course '6.01'
Staff601
building 34
pat
room 501

E3
name 'Pat'
self
E4 age 60
role 'Professor'
building 32
room 'G492'
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 73

Note (especially for Java programmers!) that the way the body of salutation has access to the
attributes of the instance on which it is operating and to attributes of the class is via the instance
we passed to it as the parameter self. The parent environment is E1, which means that methods
cannot simply use the names of class attributes without accessing them through the instance.
The notation

Staff601.salutation(pat)

is a little clumsy; it requires that we remember to what class pat belongs, in order to get the
appropriate salutation method. We ought, instead, to be able to write

pat.salutation(pat) #### Danger: not legal Python

This should have exactly the same result. (Verify this for yourself if you do not see it, by tracing
through the environment diagrams. Even though the pat object has no binding for saluation,
the environment-lookup process will proceed to its parent environment and find a binding in the
class environment.)
But this is a bit redundant, having to mention pat twice. So, Python added a special rule that
says: If you access a class method by looking in an instance, then that instance is automatically
passed in as the first argument of the method.
So, we can write

pat.salutation()

This is exactly equivalent to

Staff601.salutation(pat)

A consequence of this decision is that every method must have an initial argument that is an
instance of the class to which the method belongs. That initial argument is traditionally called
self, but it is not necessary to do so.
Here is a new method. Imagine that Staff601 instances have a numeric salary attribute. So,
we might say

pat.salary = 100000

Now, we want to write a method that will give a 6.01 staff member a k-percent raise:

class Staff601:
course = ’6.01’
building = 34
room = 501

def salutation(self):
return self.role + ’ ’ + self.name

def giveRaise(self, percentage):


self.salary = self.salary + self.salary * percentage

As before, we could call this method as

Staff601.giveRaise(pat, 0.5)
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 74

or we could use the short-cut notation and write:

pat.giveRaise(0.5)

This will change the salary attribute of the pat instance to 150000. 23

3.5.3 Initialization
When we made the pat instance, we first made an empty instance, and then added attribute
values to it. Repeating this process for every new instance can get tedious, and we might wish
to guarantee that every new instance we create has some set of attributes defined. Python has a
mechanism to streamline the process of initializing new instances. If we define a class method
with the special name __init__, Python promises to call that method whenever a new instance
of that class is created.
We might add an initialization method to our Staff601 class:

class Staff601:
course = ’6.01’
building = 34
room = 501

def __init__(self, name, role, years, salary):


self.name = name
self.role = role
self.age = years
self.salary = salary

def salutation(self):
return self.role + ’ ’ + self.name

def giveRaise(self, percentage):


self.salary = self.salary + self.salary * percentage

Now, to create an instance, we would do: 24

pat = Staff601(’Pat’, ’Professor’, 60, 100000)

Here is a diagram of the environments when the body of the __init__ procedure is about to be
executed:

23
Something to watch out for!!! A common debugging error happens when you: make an instance of a class (such as
our pat above); then change the class definition and re-evaluate the file; then try to test your changes using your old
instance, pat. Instances remember the definitions of all the methods in their class when they were created. So if you
change the class definition, you need to make a new instance (it could still be called pat) in order to get the new
definitions.
24
We called the fourth formal parameter years, when age would have been clearer, just to illustrate that the names of
formal parameters do not have to match the attributes to which they are bound inside the object.
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 75

Procedure11
(self, name, role years, salary)
self.name = name
...

__init__
Procedure10
giveRaise (self, percentage)
E1 E2 salutation self.salary = self.salary + \
self.salary * percentage
course '6.01'
Staff601
building 34
pat
room 501 Procedure9
(self)
return self.role + ' ' \
E3 + self.name

self
E4 name 'Pat'
role 'Professor'
years 60
salary 100000

Note that the formal parameter self has been bound to the newly-created instance. Here is the
situation after the initialization method has finished executing:

Procedure11
(self, name, role years, salary)
self.name = name
...

__init__
Procedure10
giveRaise (self, percentage)
E1 E2 salutation self.salary = self.salary + \
self.salary * percentage
course '6.01'
Staff601
building 34
pat
room 501 Procedure9
(self)
return self.role + ' ' \
E3 + self.name
name 'Pat'
role 'Professor'
age 60
salary 100000

This method seems very formulaic, but it is frequently all we need to do. To see how initialization
methods may vary, we might instead do something like this, which sets the salary attribute based
on the role and age arguments passed into the initializer.

class Staff601:
def __init__(self, name, role, age):
self.name = name
self.role = role
if self.role == ’Professor’:
self.salary = 100000
elif self.role = ’TA’:
self.salary = 30000
else:
self.salary = 10000
self.salary = self.giveRaise((age - 18) * 0.03)
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 76

3.5.4 Inheritance
We see that we are differentiating among different groups of 6.01 staff members. We can gain
clarity in our code by building that differentiation into our object-oriented representation using
subclasses and inheritance.
At the mechanism level, the notion of a subclass is very simple: if we define a class in the following
way:

def class <className>(<superclassName):


<body>

then, when the interpreter makes the environment for this new class, it sets the parent pointer of
the class environment to be the environment named by <superclassName>.
This mechanism allows us to factor class definitions into related, interdependent aspects. For
example, in the 6.01 staff case, we might have a base class where aspects that are common to all
kinds of staff are stored, and then subclasses for different roles, such as professors:

class Staff601:
course = ’6.01’
building = 34
room = 501

def giveRaise(self, percentage):


self.salary = self.salary + self.salary * percentage

class Prof601(Staff601):
salary = 100000

def __init__(self, name, age):


self.name = name
self.giveRaise((age - 18) * 0.03)

def salutation(self):
return ’Professor’ + self.name

Let’s trace what happens when we make a new instance of Prof601 with the expression

Prof601(’Pat’, 60)

First a new environment, E4, is constructed, with E3 (the environment associated with the class
Prof601 as its parent). Here is the memory picture now:
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 77

Procedure10
giveRaise (self, percentage)
E1 E2 course '6.01' self.salary = self.salary + \
self.salary * percentage
building 34
Staff601
room 501
Prof601
Procedure9
pat
(self)
E3 return 'Professor ' \
+ self.name
salutation
__init__
Procedure11
salary 100000 (self, name, age)
self.name = name
self.raise((age-18)*0.03)

E4

As soon as it is created, the __init__ method is called, with the new environment E4 as its
first parameter and the rest of its parameters obtained by evaluating the expressions that were
passed into to Prof601, in this case, ’Pat’ and 60. Now, we need to make the procedure-call
environment, binding the formal parameters of Procedure11; it is E5 in this figure:

E2 Procedure10
giveRaise (self, percentage)
E1 course '6.01' self.salary = self.salary + \
self.salary * percentage
building 34
Staff601
room 501
Prof601
Procedure9
pat
(self)
E3 return 'Professor ' \
+ self.name
salutation
__init__
Procedure11
E5 salary 100000 (self, name, age)
self.name = name
self self.raise((age-18)*0.03)
name 'Pat'
E4
age 60

We evaluate the body of Procedure11 in E5. It starts straightforwardly by evaluating

self.name = name

which creates a binding from name to ’Pat’ in the object named self, which is E4. Now, it
evaluates

self.giveRaise((age - 18) * 0.03)

in E5. It starts by evaluating self.giveRaise. self is E4, so we look for a binding of


giveRaise. It is not bound in E4, so we look in the parent E3; it is not bound in E3 so we
look in E2 and find that it is bound to Procedure10. We are taking advantage of the fact that
raises are not handled specially for this individual or for the subclass of 6.01 professors, and
use the definition in the general class of 6.01 staff. The interpreter evaluates the argument to
Procedure10, (60 - 18) * 0.03, getting 1.26.
It is time, now, to call Procedure10. We have to remember that the first argument will be the
object through which the method was accessed: E4. So, we make a binding environment for the
parameters of Procedure10, called E6:
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 78

E2 Procedure10
giveRaise (self, percentage)
E1 course '6.01' self.salary = self.salary + \
self.salary * percentage
building 34
Staff601
room 501
Prof601
Procedure9
pat
(self)
E3 return 'Professor ' \
+ self.name
salutation
__init__
Procedure11
salary 100000 (self, name, age)
E5 self.name = name
self self.raise((age-18)*0.03)
name 'Pat'
age 60
E4 name 'Pat'
E6
self
percentage 1.26

Now the fun really starts! We evaluate self.salary = self.salary + self.salary *


percentage in E6. We start by evaluating the right hand side: self is E4, self.salary is
100000, and percentage is 1.26, so the right-hand side is 226000. Now, we assign self.salary,
which means we make a binding in E4 for salary, to 22600. It is important to see that, within a
method call, all access to attributes of the object, class, or superclass goes through self. It is not
possible to ’see’ the definition of the building attribute directly from the giveRaise method:
if giveRaise needed to depend on the building attribute, it would need to access it via the
object, with self.building. This guarantees that we always get the definition of any attribute
or method that is appropriate for that particular object, which may have overridden its definition
in the class.
When the procedure calls are all done, the environments are finally like this:

E2 Procedure10
giveRaise (self, percentage)
E1 course '6.01' self.salary = self.salary + \
self.salary * percentage
building 34
Staff601
room 501
Prof601
Procedure9
pat
(self)
E3 return 'Professor ' \
+ self.name
salutation
__init__
Procedure11
salary 100000 (self, name, age)
self.name = name
self.raise((age-18)*0.03)

E4 name 'Pat'
salary 226000

It is useful to back up and see the structure here:


• The instance pat is an environment, E4.
• The parent of the instance is an environment,E3, which is the class Prof601.
• The parent of the class is an environment, E2, which the superclass Staff601.
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 79

3.6 Recursion
There are many control structures in Python, and other modern languages, which allow you write
short programs that do a lot of work. In this section, we discuss recursion, which is also a way to
write programs of arbitrary complexity. It is of particular importance here, because the structure
of a language interpreter is recursive, and in the next section we will explore, in detail, how to
construct an interpreter.
We have seen how we can define a procedure, and then can use it without remembering or caring
about the details of how it is implemented. We sometimes say that we can treat it as a black box,
meaning that it is unnecessary to look inside it to use it. This is crucial for maintaining sanity
when building complex pieces of software. An even more interesting case is when we can think
of the procedure that we are in the middle of defining as a black box. That is what we do when
we write a recursive procedure.
Recursive procedures are ways of doing a lot of work. The amount of work to be done is con-
trolled by one or more arguments to the procedure. The way we are going to do a lot of work
is by calling the procedure, over and over again, from inside itself! The way we make sure this
process actually terminates is by being sure that the argument that controls how much work we
do gets smaller every time we call the procedure again. The argument might be a number that
counts down to zero, or a string or list that gets shorter.
There are two parts to writing a recursive procedure: the base case(s) and the recursive case.
The base case happens when the thing that is controlling how much work you do has gotten to
its smallest value; usually this is 0 or the empty string or list, but it can be anything, as long as
you know it is sure to happen. In the base case, you just compute the answer directly (no more
calls to the recursive procedure!) and return it. Otherwise, you are in the recursive case. In the
recursive case, you try to be as lazy as possible, and foist most of the work off on another call to
this procedure, but with one of its arguments getting smaller. Then, when you get the answer
back from the recursive call, you do some additional work and return the result.
Another way to think about it is this: when you are given a problem of size n, assume someone
already gave you a way to solve problems of size n − 1. So, now, your job is only to figure out
• To which problem of size n − 1 you would like to know the answer, and
• How to do some simple operations to make that answer into the answer to your problem of
size n.

What if you wanted to add two positive numbers, but your computer only had the ability to
increment and decrement (add and subtract 1)? You could do this recursively, by thinking about
it like this. I need to add m and n:
• Presupposing the ability to solve simpler problems, I could get the answer to the problem m
plus n-1.
• Now, I just need to add 1 to that answer, to get the answer to my problem.
We further need to reason that when n is 0, then the answer is just m. This leads to the following
Python definition:

def slowAdd(m, n):


if n == 0:
return m
else:
return 1 + slowAdd(m, n-1)
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 80

Note how in the final return expression, we have reduced the answer to a problem of size n to a
simpler version of the same problem (of size n-1 plus some simple operations.
Here is an example recursive procedure that returns a string of n 1’s:

def bunchaOnes(n):
if n == 0:
return ’’
else:
return bunchaOnes(n-1) + ’1’

The thing that is getting smaller is n. In the base case, we just return the empty string. In the
recursive case, we get someone else to figure out the answer to the question of n-1 ones, and then
we just do a little additional work (adding one more ’1’ to the end of the string) and return it.

Exercise 3.16. What is the result of evaluating

bunchaOnes(-5)

Here is another example. It is kind of a crazy way to do multiplication, but logicians love it.

def mult(a,b):
if a==0:
return 0
else:
return b + mult(a-1,b)

Trace through an example of what happens when you call mult(3, 4), by adding a print state-
ment that prints arguments a and b as the first line of the procedure, and seeing what happens.
Here is a more interesting example of recursion. Imagine we wanted to compute the binary repre-
sentation of an integer. For example, the binary representation of 145 is ’10010001’. Our procedure
will take an integer as input, and return a string of 1’s and 0’s.

def bin(n):
if n == 0:
return ’0’
elif n == 1:
return ’1’
else:
return bin(n/2) + bin(n%2)

The easy cases (base cases) are when we are down to a 1 or a 0, in which case the answer is
obvious. If we do not have an easy case, we divide up our problem into two that are easier.
So, if we convert n/2 (the integer result of dividing n by 2, which by Python’s definition will be
a smaller number since it truncates the result, throwing away any remainder), into a string of
digits, we will have all but the last digit. And n%2 (n modulo 2) is 1 or 0 depending on whether
the number is even or odd, so one more call of bin will return a string of ’0’ or ’1’. The other thing
that is important to remember is that the + operation here is being used for string concatenation,
not addition of numbers.
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 81

How do we know that this procedure is going to terminate? We know that the number on which
it is operating is a positive integer that is getting smaller and smaller, and will eventually be either
a 1 or a 0, which can be handled by the base case.
You can also do recursion on lists. Here a way to add up the values of a list of numbers:

def addList(elts):
if elts == []:
return 0
else:
return elts[0] + addList(elts[1:])

The addList procedure consumed a list and produced a number. The incrementElements
procedure below shows how to use recursion to do something to every element of a list and make
a new list containing the results.

def incrementElements(elts):
if elts == []:
return []
else:
return [elts[0]+1] + incrementElements(elts[1:])

If the list of elements is empty, then there is no work to be done, and the result is just the empty
list. Otherwise, the result is a new list: the first element of the new list is the first element of the
old list, plus 1; the rest of the new list is the result of calling incrementElement recursively on
the rest of the input list. Because the list we are operating on is getting shorter on every recursive
call, we know we will reach the base case, and all will be well.

3.7 Implementing an interpreter


This section can be skipped unless you are interested. It’s very cool but a bit tricky.

From the preceding sections, you should have an informal understanding of what happens
when a Python program is evaluated. Now, we want to understand it formally enough to actually
implement an interpreter in Python.

3.7.1 Spy
We will study a simplified language, which we call Spy, because it is a mixture of Scheme and
Python. From Scheme, an elegant interpreted programming language with very simple syntax
and semantics, we take the syntax of the language, and from Python, we take the basic object-
oriented programming system. Spy has a small subset of the features of these languages, but it
is powerful enough to implement any computer program that can be implemented in any other
language (though it could be mighty tedious).

To learn more: Interestingly, very minimal versions of several different models of com-
putation (imperative, functional/recursive, rewrite systems) have been
shown to be formally equivalent. Some have theorized them to be com-
plete in the sense that there are no computations that cannot be expressed
this way. For more information, see:
http://plato.stanford.edu/entries/church-turing/
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 82

The syntax of Spy, like Scheme, is fully-parenthesized prefix syntax. Prefix means that the name
of the function or operation comes before its arguments, and fully-parenthesized means that there
are parentheses around every sub-expression. So, to write 1 + num * 4, we would write

(+ 1 (* num 4)) .

The reason for adopting this syntax is that it is very easy to manipulate with a computer program
(although some humans find it hard to read). In the following, we will assume that someone
has already written a tokenizer, which is a program that can consume a stream of characters and
break it into “words” for us. In particular, we will assume that the tokenizer can break an input
Spy program down into a list of lists (of lists...of elements that are strings or integers). So the
expression (+ 1 (* num 4)) would be converted by the tokenizer into a representation such as:
(’+’, 1, (’*’, ’num’, 4)).
Spy has the following features:
• Integer constants:

0, 1, -1, 2, -2, ...


• Basic built-in functions:

+, -, *, /, =, with meanings you would expect; note that in this language = is a test for equality
on two integers, which returns a Boolean.
• Assignment:

(set a 7) will set (or assign) the value of variable a to be 7


• Function application:

A list of expressions, the first of which is not a special word in Spy, is treated as function
application, or function call. So, (+ 3 a) would return 3 plus the value of variable a, and
(f) is a call of a function f with no arguments. Note that the first element can, itself, be an
expression, so

((myFunctionMaker 3) (+ 4 5))

is also a valid expression, as long as the value of (myFunctionMaker 3) is a function that will
consume a single argument (which, in this case, will be the value of the function application
(+ 4 5)).
• Function definition:

New functions can be defined, much as in Python or Scheme. So,

(def myFun (x y) (* (+ x y) y))

defines a new function named myFun of two arguments, which can be applied with an expres-
sion like (myFun 4 9).
• If:

(if a b c) will evaluate and return the value of expression b if the value of expression a is
equal to True, and otherwise will evaluate and return the value of expression c.
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 83

• Compound expression:
In Spy, the body of a function definition, and the branches of an if expression, are expected to
be a single expression; in order to evaluate multiple expressions in one of those locations, we
need to group them together with begin, like this:
(begin (set a 7) (set b 8) (+ a b)) .

(The reason we need begin to be a special word is so that we can distinguish this list of
expressions from a function application, which is also, syntactically, a list of expressions.) The
value of a compound expression is the value of its last component. So, in our example, the
value of the whole expression would be 15.
Note that the names begin, set, def, and if have special meaning in Spy and cannot be used as
the names of user-defined functions.
Spy also has some object-oriented features, but we will introduce those in section 3.7.3.

Exercise 3.17. Write a Spy procedure that takes two positive integers as input and returns
True if the first is greater than the second. Use only the primitive functions
= and + and recursion.

Exercise 3.18. Write a Spy procedure that takes n as input and returns the nth number in
the Fibonacci series.

3.7.2 Evaluating Spy expressions


In this section, we will describe, in complete detail, how it is that a program in Spy can be ex-
ecuted. The basic operations are evaluating expressions, and assigning values to names. In the
following sections, we will develop a recursive definition that allows us to compute the value
resulting from any Spy program. We will start with simple expressions, and work up to complex
ones.
The spyEval function will consume an expression and some additional information, and return
the value of the expression. It will have the structure of a long set of if-elif-else clauses, each of
which tests to see whether the expression has a particular form and performs the appropriate
evaluation.

To learn more: An interpreter is a program that takes in the text of a computer program
and executes it directly. We are going to build an interpreter for Spy in this
section, and when we use Python, we use an interpreter. A compiler is a
program that takes in the text of a computer program and turns it into low-
level instructions for a particular computer, which can later be executed.
A gross characterization of the difference is that it is easier to debug when
working with an interpreter, but that your program runs faster when com-
piled. For more information start with Wikipedia articles on Compiler and
Interpreter_(computing).
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 84

3.7.2.1 Numbers
What is the value of the program ’7’? It’s 7, of course. So, we can begin defining a function that
will consume a Spy expression (described as a list of lists of strings) and return a value.

Spy Eval def spyEval(form, env):


if isinstance(form, int):
return form

Here, we need to be able to examine an expression to see if it is an integer (the isinstance


procedure in Python takes any Python object and a type or class, and returns True if the object
is of that type or class, and False otherwise). If it is an integer, then we just return that integer
value. (You might have imagined that form would be a string, and that we’d have to convert that
string to an integer; in fact, the tokenizer, when it finds a token that is a number, converts it for us
in advance.)
For now, ignore the env argument; we’ll explain it soon.

3.7.2.2 Compound expressions


As we discussed above, a list of expressions, where the first one is the word ’begin’ is a com-
pound expression, whose value is the value of the last component. But, for reasons we will see in
section 3.7.2.3, it is important to evaluate all of the expressions, not just the last one. So, we can
extend our definition of spyEval by adding another clause (the ellipsis below is meant to include
the text from the previous spyEval code):

Spy Eval ...


elif form[0] == ’begin’:
val = None
for entry in form[1:]:
val = spyEval(entry, env)
return val

3.7.2.3 Symbols
We’ll use the term symbol to refer to any syntactic item that isn’t a number or parenthesis.
What is the value of the program ’a’? All by itself, it is an error, because ’a’ is undefined. So,
let’s consider a more complicated program:

(begin (set a 6)
a)

Why would we ever want to evaluate expressions and throw their values away? In a pure func-
tional language, the answer is that we wouldn’t. But in Spy, we have assignment expressions.
They don’t even have a value, but they make a change to the environment in which they are eval-
uated. Before we go any further, we’ll have to take a slight detour, in order to understand the idea
of environments.
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 85

Environments
An environment consists of two parts:
• a dictionary
• a parent environment (can be None)
The dictionary is a data structure that lets us associate values (numbers, functions, objects, etc.)
with symbols; a Python dictionary works perfectly for this purpose.
We will say that a symbol is bound in an environment if it exists as a key in the environment’s
dictionary. We’ll sometimes call the relationship between a key and a value in an environment’s
dictionary a binding.
There are three operations we can do on an environment: look up a symbol, add a binding, and
extend an environment. We’ll define the first two here, and return to environment extension later.
The lookup operation takes a symbol and an environment, and performs a slightly generalized
version of a dictionary lookup, looking in a parent environment if the symbol is not in the dictio-
nary:
• If the symbol is a key in the environment’s dictionary, then it returns the associated value;
• Otherwise, if this environment has a parent, then it returns the result of looking the symbol
up in the parent environment;
• Otherwise, it generates an error.
The add binding operation takes a symbol, an environment, and a value; it adds the value to the
dictionary associated with the environment, using the symbol as the key. If there was already a
value associated with that symbol in the dictionary, the old value is overwritten by the new value.

’a’ 9
’c’ 12

’a’ 6
’b’ 7

Figure 3.1 A simple environment.

Exercise 3.19. If e is the environment in Figure 3.1, what are the results of:
• e.lookup(’a’)
• e.lookup(’c’)
• e.lookup(’d’)
• e.add(’f’, 1)
• e.add(’c’, 2)
• e.add(’a’, 2)

Where do environments come from? There is one environment that is made by the interpreter,
called global. It contains bindings for the primitive procedures, and is the environment in which
our main program is evaluated.
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 86

Assigning values
So, let’s go back to the expression

(set a 6)

We’ll refer to the second and third elements of this expression (the ’a’ and the 6) as the left-hand-
side (lhs) and right-hand-side (rhs) respectively. 25 For now, the lhs will always be a symbol. Now,
to evaluate this expression in an environment, we evaluate the expression that is the rhs to get a
value, and then bind the symbol that is the lhs to that value in the environment.
So, here’s our augmented definition of eval. Note that it we have added the env argument, which
is the environment in which we’re evaluating the expression:

Spy Eval elif form[0] == ’set’:


(tag, lhs, rhs) = form
env.add(lhs, spyEval(rhs, env))

So, in our simple example, lhs is the symbol ’a’; expr is 6, which does not need further eval-
uation (the tokenizer has already turned the string ’6’) into an internal Python integer 6. More
generally, though, the expr can be an arbitrarily complicated expression which might take a great
deal of work to evaluate.
After we have evaluated that expression, our global environment will look like this:

global
’a’ 6
’+’ <func>
..
.

Evaluating symbols
Now, finally, we can evaluate the expression ’a’. All we do to evaluate a symbol is look it up in
the environment. So, the return value of this whole program

(begin (set a 6)
a)

is the value 6.
So, now, we can add another clause to spyEval:

Spy Eval ...


elif isinstance(form, str):
return env.lookup(form)

25
That usage comes because a typical assignment statement has the form lhs = rhs.
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 87

3.7.2.4 Functions
In the Spy language, any list of expressions, the first element of which is not a special symbol is
treated as a function call. 26 Each of the expressions in the list is evaluated, yielding a function and
some number of values.
Before we go any further, we have to talk about functions in some detail. There are two types
of functions: primitive functions and user defined functions. Primitive functions come built into
the interpreter and are, ultimately, implemented as part of the interpreter. Spy provides several
primitive functions that are bound to names in the global environment. So, to call a primitive
function, all we have to do is pass the values we obtained by evaluating the rest of the expressions
in the function call into the primitive function.

Function definitions
Before we can talk about calling user-defined functions, we have to talk about how to define them.
Here is an example definition:

(def fizz (x y)
(+ x y))

It is a list of four elements:


1. def, which is a special symbol, indicating that this expression is not a function call, and there-
fore requires special handling;
2. the name of the function being defined (’fizz’ in this case);
3. a list of symbols, called the formal parameters (in this example, (’x’, ’y’); and
4. a function body, which is a Spy expression, in this case, ’(+ x y)’
Not very much happens at function definition time. We construct a data structure (typically an
instance of a Function class) that simply stores three components:
• the formal parameters
• the function body
• the environment in which the function definition was evaluated
Then, we add a binding in the environment in which the function was defined from the func-
tion name to this function structure. With this understanding of function definition, we can add
another clause to spyEval:

Spy Eval ...


elif form[0] == ’def’:
(tag, name, params, body) = form
env.add(name, Function(params, body, env))

Here is a picture of the global environment after the definition of fizz is made. Note the binding
from the name ’fizz’ to a Function instance which, itself, contains a reference to the environ-
ment.

26
So far, our special symbols are begin and set, and we’ll add def and if.
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 88

global Procedure1
(x, y)
fizz (+ x y)

Function calls
Now, we can see what happens when we actually call a function. Let’s assume that the function
fizz from the previous section has been defined, and now we want to evaluate

(fizz 6 8)

in the environment env. This is the trickiest part of the whole interpreter. First, we evaluate each
of these expressions, getting: a Function instance (which we created when we defined the fizz
function), the number 6 and the number 8.
Now, we make a new environment. Let’s call it (imaginatively) newEnv. The dictionary part of
newEnv is used to associate the symbols that are the formal parameters of the function (in the
case of the fizz function, ’x’ and ’y’) with the actual values being passed into the function (in
this case, the numbers 6 and 8). The parent environment of newEnv is the environment that we
stored with the function when it was defined (not the environment in which the function is being
called!!). In this case, fizz was defined in the global environment. And so, the parent pointer is
also the global environment. (Later, we may see some examples where this pointer is different, in
a way that matters.)
Here is a diagram of the entire environment, at this point:

global Procedure1
(x, y)
fizz (+ x y)

x 6
y 8

Now that we have this new environment, we evaluate the expression that is the body of the
function, in this case, ’(+ x y)’ in newEnv. This will all work out roughly as you would expect:
during the process of evaluating the body, we will have to evaluate the symbols ’+’, ’x’ and
’y’. We will find ’x’ and ’y’ in the dictionary of newEnv; and we will find ’+’ in its parent
environment, which is global. So, we’ll have the function object that is bound to ’+’, and the
number 6 and the number 8; then, we’ll call the primitive function on these values, and end up
with the result 14.
Here is our augmented definition of spyEval:
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 89

Spy Eval ...


elif isinstance(form, list):
f = spyEval(form[0], env)
return spyEval(f.body,
Environment(f.formal,
[spyEval(x, env) for x in form[1:]],
f.environment))

As required, this process evaluates the first expression in the list to get a function. Then it evalu-
ates the body of that function in a new environment that maps the function’s formal parameters
to the results of evaluating all the rest of the expressions in the list, and whose parent is the envi-
ronment stored in the function.
A good way to understand what is going on with spyEval is to show a trace of its behavior. So,
when we evaluate the following expression:

(begin (def fizz (a b)


(+ a b))
(fizz 3 4))

we can print out the arguments that are passed into spyEval (though we have left out the envi-
ronment, to keep things from being too cluttered), and the result of each call. Each time it is called
recursively, we print out the arguments and results with one level more of indentation:

args: [’begin’, [’def’, ’fizz’, [’a’, ’b’], [’+’, ’a’, ’b’]], [’fizz’, 3, 4]]
args: [’def’, ’fizz’, [’a’, ’b’], [’+’, ’a’, ’b’]]
result: None
args: [’fizz’, 3, 4]
args: fizz
result: <__main__.Function instance at 0x7b1e18>
args: 3
result: 3
args: 4
result: 4
args: [’+’, ’a’, ’b’]
args: +
result: Primitive <built-in function add>
args: a
result: 3
args: b
result: 4
result: 7
result: 7
result: 7

Here is another version, showing the basic part of the environment, but not the parent environ-
ment.

args: [’begin’, [’def’, ’fizz’, [’a’, ’b’], [’+’, ’a’, ’b’]], [’fizz’, 3, 4]] Env: global
args: [’def’, ’fizz’, [’a’, ’b’], [’+’, ’a’, ’b’]] Env: global
result: None
args: [’fizz’, 3, 4] Env: global
args: fizz Env: global
result: <__main__.Function instance at 0x7b1df0>
args: 3 Env: global
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 90

result: 3
args: 4 Env: global
result: 4
args: [’+’, ’a’, ’b’] Env:{’a’: 3, ’b’: 4}
args: + Env:{’a’: 3, ’b’: 4}
result: Primitive <built-in function add>
args: a Env:{’a’: 3, ’b’: 4}
result: 3
args: b Env:{’a’: 3, ’b’: 4}
result: 4
result: 7
result: 7
result: 7

3.7.2.5 If
The last special type of expression in Spy is a conditional, of the form:

(if (= x 3)
(fizz x 10)
(+ x 4))

It might seem, at first, that it would be okay to implement if as a primitive function, similar to +.
But there is an important reason not to do so. Consider the definition of the factorial function, in
Spy:

(def factorial (x)


(if (= x 1)
1
(* x (factorial (- x 1)))))

The most straightforward application of this function, (factorial 1) will get us into trouble.
Why? We would start by evaluating ’factorial’ and ’1’, and getting a function and 1. So
far so good. Now we make a new environment, and evaluate the body in that environment.
That requires evaluating all of the elements of the body in that environment. So, we’d find that
’if’ evaluates to a primitive function, that (= x 1) evaluates to True, and that 1 evaluates to 1.
Then, we’d need to evaluate that last expression, which is itself a function call. That, in itself, is no
problem. But we can see that eventually, we’ll have to evaluate factorial in an environment where
its argument is -1. And that will require evaluating factorial of -2, which will require evaluating
factorial of -3, and so on.
The most important thing about ’if’ is that it evaluates only one of its branches, depending on
whether its condition is True or False. Without this property, we cannot stop recursion.
So, now we can see what to do with an ’if’ expression. We’ll call the second, third, and fourth
elements the condition, then and else parts. We start by evaluating the condition part. If it is
True, we evaluate the then part, otherwise we evaluate the else part.
It is pretty straightforward to add this to our eval function; but note that we had to put it before
the function application clause, so that we can keep if expressions from being evaluated as if
they were function applications.
Here is the whole interpreter.
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 91

Spy Eval def spyEval(form, env=globalEnv):


if isinstance(form, int):
return form
elif isinstance(form, str):
return env.lookup(form)
elif form[0] == ’begin’:
val = None
for entry in form[1:]:
val = spyEval(entry, env)
return val
elif form[0] == ’set’:
(tag, lhs, rhs) = form
env.add(lhs, spyEval(rhs, env))
elif form[0] == ’def’:
(tag, name, params, body) = form
env.add(name, Function(params, body, env))
elif form[0] == ’if’:
(tag, condition, ifBody, elseBody) = form
if spyEval(condition, env)
return spyEval(ifBody, env)
else:
return spyEval(elseBody, env)
elif isinstance(form, list):
f = spyEval(form[0], env)
return spyEval(f.body,
Environment(f.formal,
[spyEval(x, env) for x in form[1:]],
f.environment))
else:
Error("Illegal expression: "+str(form))

Yay! Now we have a complete Spy interpreter. Well, we still need implementations of the Envi-
ronment and Function classes, but that’s not very much more work. We can implement those
classes as exercises.

3.7.3 Object-Oriented Spy


We can add a simple object-oriented facility to Spy, which is modeled on Python’s OOP facility,
but is somewhat simpler. The crucial idea here is that classes and instances are both environments,
of exactly the same kind that we have been using to support binding and function calls in basic
Spy. The dictionary part of the environment supports the binding of attribute names to values
within the instance or class; and the parent environment part allows an instance to be connected
to its class, or a class to its superclass, so that attributes that are not defined within the instance
may be found in the class or superclass.
We only have to add two new syntactic features to the language: attribute lookup and class defi-
nition.

3.7.3.1 Attribute lookup


In Python, when you want to get the value of an attribute a from an instance obj, you say obj.a.
In OOSpy, we’ll say (attr obj a). Remembering that an instance is an environment, all we
have to do to evaluate such an expression is to look up the symbol ’a’ in the environment ’obj’.
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 92

Referring to the second part of this form as the object part and the third as the name part, we can
add a clause for attr expressions to our interpreter. The name will always be a single symbol;
but the object part can be a general expression (we might, for example, call a function bigFun that
returns an object, yielding an expression like (attr (bigFun 3) x) to get the attribute named
x from the object returned by bigFun). This means that we have to evaluate the object part, using
our standard evaluation function. Now, we can add a clause for attr expressions to our spyEval:

Spy Eval ...


elif form[0] == ’attr’:
(tag, objectExpr, name) = form
return spyEval(objectExpr, env).lookup(name)
...

3.7.3.2 Class definition


All the rest of our work happens at class definition time. Here is a simple OOSpy class definition:

(class SimpleClass None


(begin (def init (self v)
(set (attr self v) v))
(def getV (self)
(attr self v))))

For reference, it is roughly equivalent to the Python:

class SimpleClass:
def init (self, v):
self.v = v
def getV (self):
return self.v

It has four parts: the special symbol ’class’, the class name SimpleClass, the name of a su-
perclass (in this case, there is no superclass, so we write None), and a class body, which is a Spy
expression. Our simple Spy implementation doesn’t have an equivalent to Python’s __init__
facility, in which the specially-named procedure is called automatically. So we call the method
init without underscores to make that clear.
Here is a picture of the environment that we would like to have result from this definition:

Procedure1
()
return Environment( )
Procedure2
(self,v)
self.v = v
init
global getV
Procedure3
SimpleClass (self)
return self.v

Note that Spy differs from Python here in an important way: in Python, the environment stored
in a method is always the module environment; in Spy, to avoid having a special case, it is the
class environment in which it is defined.
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 93

Also, rather than binding SimpleClass directly to the environment for the class, we will bind
it to a procedure that makes a new environment whose parent is the class environment. This
procedure can be called directly to create a new instance of the class.
There are three steps involved in processing this class definition.
1. Make an environment for the class In this step, we just make a new empty environment, and
set its parent to be the class specified as the superclass of this class. If the superclass is None,
then we use global as the parent environment, so that global variables and primitive function
definitions will be accessible from the class body.
2. Evaluate the class body in the class environment In this step, we use our regular Spy eval-
uation mechanism, to evaluate the class body, in the new environment. In our SimpleClass
body, we define two methods; these definitions will appear in the class environment but will
not, for example, be accessible directly from the global environment.
3. Make a constructor function Finally, we define a new function, with the same name as the
class, in the environment in which the class definition is being evaluated (not the class envi-
ronment; if we did that, nobody would every be able to find the constructor!). The constructor
function has the job of making a new instance of this class. What is an instance? An environ-
ment. And a brand new instance of any class is simply an empty environment, whose parent
environment is the class environment.
After defining SimpleClass, we can use it as follows:

(begin
(set a (SimpleClass))
((attr a init) a 37)
((attr a getV) a))

This is roughly equivalent to the Python

a = SimpleClass()
a.init(37)
a.getV()

This may look a little bit strange. Let’s go step by step. First, we make an instance by calling the
constructor function. That’s pretty straightforward. Now, the value of variable a is an instance of
the SimpleClass class, which means that it is an environment.
Now, what about this expression?

((attr a init) a 37)

Let’s try to work through it the way the interpreter would. It is a list, and the first element is not
a special symbol, so it must be a function call. That means we should evaluate each of the forms
in the list.
First we evaluate (attr a init), which is a list starting with the special symbol attr, so we
know it’s looking up an attribute in a class. It evaluates the expression a, getting the SimpleClass
instance, then looks for the init attribute, which yields the init function that we defined inside
the class body.
Evaluating the second and third elements of ((attr a init) a 37) yield our SimpleClass
instance and the number 37.
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 94

Now, we’re ready to do a function call. Let’s do it carefully. The formal parameters of the function
we’re calling are (’self’, ’v’). So, we make a new environment, in which those parameters
are bound to the SimpleClass instance and to 37, respectively, and whose parent environment
is the environment in which ((attr a init) a 37) is being evaluated. Now, we evaluate the
body of that function, which is

(set (attr self v) v)

in this new environment. Let’s call this environment E1. In order to understand what’s going on
here, we have to go slowly and carefully. Here’s E1:

Procedure1
()
return Environment( )
Procedure2
(self,v)
self.v = v
init
global getV Procedure3
SimpleClass (self)
return self.v
a

E1
self
v 37

First, we recognize this as a set expression. But, so far, our set expressions have always had a
single symbol as their second element. Now, we will have to generalize it, so the second element
of a set expression can also be an attr expression. Let’s consider this kind of expression, in
general:

(set (attr x y) z)

In this form, x can be any expression that, when evaluated, yields an instance; y must be a single
symbol; and z can be any expression that, when evaluated, yields a value. The result of this
form is to set attribute y in the instance resulting from evaluating expression x to have the value
resulting from evaluating the expression z.
So, we need to evaluate (set (attr self v) v) in the environment E1. We start by evaluating
self, and getting the SimpleClass instance; we look for an attribute named v there and, not
finding one, make a new one. Now, we evaluate the third part of the set expression, which is v,
in the E1, and get the value 37. Finally, we set attribute v of our instance to 37. Yay. Here’s the
new state of the environment:
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 95

Procedure1
()
return Environment( )
Procedure2
(self,v)
self.v = v
init
global getV Procedure3
SimpleClass (self)
return self.v
a

E1
self
v 37

One thing to notice is that Spy, as we have designed it, doesn’t have Python’s special facility that
automatically uses an object as the first argument of a method, when that method is accessed via
that object. So, we have to pass the object in again, explicitly, as the first argument.

Exercise 3.20.
Add this syntactic facility to OOSpy.
That is, make it so that we can write ((attr a init) 37) and ((attr
a getV)). This is a little bit tricky.

Evaluating the expression ((attr a getV) a) works similarly (you should be sure you under-
stand how), and returns the value 37.
So, after all that, we can add the last clause to our OOSpy interpreter, for handling class defin-
itions. Note that we also have to extend the code for handling set, so it can deal with setting
attributes of instances.

Spy Eval ...


elif form[0] == ’set’:
(tag, lhs, expr) = form
(targetEnv, name) = lhsEval(lhs, env)
targetEnv.add(name, spyEval(expr, env))
elif form[0] == ’class’:
(tag, name, super, body) = form
if super == ’None’:
super = globalEnv
classEnv = Environment(parent = super)
env.add(name, Primitive(lambda : Environment(parent = classEnv)))
spyEval(body, classEnv)
...

def lhsEval(lhs, env):


if isinstance(lhs, list):
(tag, objectExpr, name) = lhs
return (spyEval(objectExpr, env), name)
else:
return (env, lhs)
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 96

Primitive is a class that represents a primitive function in Spy. Its initializer just takes a proce-
dure as an argument: in this case, it is a procedure that makes a new environment whose parent
is the the class environment.
We’re done with a whole object-oriented language!

Exercise 3.21. Write an OOSpy class definition for an Averager class. It should have a
method addValue that allows you to “add” a numeric value to the set of
data it as seen and method getAverage that returns the average of all of
the values that have been added so far.

3.8 Object-Oriented Programming Examples


Here are some examples of object-oriented programming in Python.

3.8.1 A simple method example


Here’s an example to illustrate the definition and use of classes, instances, and methods.

class Square:
def __init__(self, initialDim):
self.dim = initialDim

def getArea (self):


return self.dim * self.dim

def setArea (self, area):


self.dim = area**0.5

def __str__(self):
return "Square of dim " + str(self.dim)

This class is meant to represent a square. Squares need to store, or remember, their dimension,
so we make an attribute for it, and assign it in the __init__ method. Now, we define a method
getArea that is intended to return the area of the square. There are a couple of interesting things
going on here.
Like all methods, getArea has an argument, self, which will stand for the instance that this
method is supposed to operate on. Now, the way we can find the dimension of the square is by
finding the value of the dim attribute of self.
We define another method, setArea, which will set the area of the square to a given value. In
order to change the square’s area, we have to compute a new dimension and store it in the dim
attribute of the square instance. 27
Now, we can experiment with instances of class Square.

27
We compute the square root of a value by raising to the power 0.5.
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 97

>>> s = Square(6)
>>> s.getArea()
36
>>> Square.getArea(s)
36
>>> s.dim
6
>>> s.setArea(100)
>>> s.dim
10.0
>>> s1 = Square(10)
>>> s1.dim
10
>>> s1.getArea()
100
>>> s2 = Square(100)
>>> s2.getArea()
10000
>>> print s1
Square of dim 10

Our class Square has the __str__ method defined, so it prints out nicely.

3.8.2 Superclasses and inheritance


What if we wanted to make a set of classes and instances that would help us to run a bank? We
could start like this:

class Account:
def __init__(self, initialBalance):
self.currentBalance = initialBalance
def balance(self):
return self.currentBalance
def deposit(self, amount):
self.currentBalance = self.currentBalance + amount
def creditLimit(self):
return min(self.currentBalance * 0.5, 10000000)

>>> a = Account(100)
>>> b = Account(1000000)

>>> Account.balance(a)
100
>>> a.balance()
100
>>> Account.deposit(a, 100)
>>> a.deposit(100)
>>> a.balance()
300
>>> b.balance()
1000000

We’ve made an Account class 28 that maintains a balance as an attribute. There are methods for
returning the balance, for making a deposit, and for returning the credit limit.

28
A note on style. It is useful to adopt some conventions for naming things, just to help your programs be more read-
able. We’ve used the convention that variables and procedure names start with lower case letters and that class
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 98

The Account class contains the procedures that are common to all bank accounts; the individual
instances contain state in the values associated with the names in their environments. That state
is persistent, in the sense that it exists for the lifetime of the program that is running, and doesn’t
disappear when a particular method call is over.
Now, imagine that the bank we’re running is getting bigger, and we want to have several different
kinds of accounts, with the credit limit depending on the account type. If we wanted to define
another type of account as a Python class, we could do it this way:

class PremierAccount:
def __init__(self, initialBalance):
self.currentBalance = initialBalance
def balance(self):
return self.currentBalance
def deposit(self, amount):
self.currentBalance = self.currentBalance + amount
def creditLimit(self):
return min(self.currentBalance * 1.5, 10000000)
>>> c = PremierAccount(1000)
>>> c.creditLimit()
1500.0

This will let people with premier accounts have larger credit limits. And, the nice thing is that we
can ask for its credit limit without knowing what kind of an account it is, so we see that objects
support generic functions, which operate on different kinds of objects by doing different, but
type-appropriate operations.
However, this solution is still not satisfactory. In order to make a premier account, we had to
repeat a lot of the definitions from the basic account class, violating our fundamental principle of
laziness: never do twice what you could do once; instead, abstract and reuse.
Inheritance lets us make a new class that’s like an old class, but with some parts overridden or
new parts added. When defining a class, you can actually specify an argument, which is another
class. You are saying that this new class should be exactly like the parent class or superclass, but
with certain definitions added or overridden. So, for example, we can say

class PremierAccount(Account):
def creditLimit(self):
return min(self.currentBalance * 1.5, 10000000)

class EconomyAccount(Account):
def creditLimit(self):
return min(self.currentBalance*0.5, 20.00)

>>> a = Account(100)
>>> b = PremierAccount(100)
>>> c = EconomyAccount(100)
>>> a.creditLimit()
100.0
>>> b.creditLimit()
150.0

names start with upper case letters. And we try to be consistent about using something called “camel caps” for
writing compound words, which is to write a compound name with the successiveWordsCapitalized. An alternative
is_to_use_underscores.
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 99

>>> c.creditLimit()
20.0

We automatically inherit the methods of our superclass (including __init__). So we still know
how to make deposits into a premier account:

>>> b.deposit(100)
>>> b.balance()
200

The fact that instances know what class they were derived from allows us to ask each instance
to do the operations appropriate to it, without having to take specific notice of what class it is.
Procedures that can operate on instances of different types or classes without explicitly taking
their types or classes into account are called polymorphic. Polymorphism is a very powerful
method for capturing common patterns.

3.8.3 A data type for times


Object-oriented programming is particularly useful when we want to define a new compositional
data type: that is, a representation for the data defining a class of objects and one or more oper-
ations for creating new instances of the class from old instances. We might do this with complex
numbers or points in a high-dimensional space.
Here we demonstrate the basic ideas with a very simple class for representing times.
We’ll start by defining an initialization method, with default values specified for the parameters.

class Time:
def __init__(self, hours = 0, minutes = 0):
self.hours = hours
self. minutes = minutes

Here is a method for adding two time objects. If the summed minutes are greater than 60, it carries
into the hours. If the summed hours are greater than 24, then the excess is simply thrown away.
It is important that adding two Times returns a new Time object, and that it does not change any
aspect of either of the arguments.

def add(self, other):


newMinutes = (self.minutes + other.minutes) % 60
carryHours = (self.minutes + other.minutes) / 60
newHours = (self.hours + other.hours + carryHours) % 24
return Time(newHours, newMinutes)

Python has a special facility for making user-defined types especially cool: if, for instance, you
define a method called __add__ for your class Foo, then whenever it finds an expression of the
form <obj1> + <obj2>, and the expression <obj1> evaluates to an object of class Foo, it will
Foo’s __add__ method. So, here we set that up for times:

def __add__(self, other):


return self.add(other)

Additionally, defining methods called __str__ and __repr__ that convert elements of your class
into strings will mean that they can be printed out nicely by the Python shell.
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 100

def __str__(self):
return str(self.hours) + ’:’ + str(self.minutes)
def __repr__(self):
return str(self)

Here, we make some times. In the second case, we specify only the minutes, and so the hours
default to 0.

>>> t1 = Time(6, 30)


>>> t2 = Time(minutes = 45)
>>> t3 = Time(23, 59)

And now, we can do some operations on them. Note how our __str__ method allows them to
be printed nicely.

>>> t1
6:30
>>> t1 + t2
7:15
>>> t3 + t2
0:44
>>> t1 + t2 + t3
7:14
>>> (t1 + t2 + t3).minutes
14

3.8.4 Practice problem: argmax


Write a Python procedure argmax that takes two arguments: the first is a list of elements, and the
second is a function from elements to numerical values. It should return the element of the list
with the highest numerical score.
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 101

def argmax(elements, f):


bestScore = None
bestElement = None
for e in elements:
score = f(e)
if bestScore == None or score > bestScore:
bestScore = score
bestElement = e
return bestElement

def argmax(elements, f):


bestElement = elements[0]
for e in elements:
if f(e) > f(bestElement):
bestElement = e
return bestElement

def argmax(elements, f):


vals = [f(e) for e in elements]
return elements[vals.index(max(vals))]

def argmax(elements, f):


return max(elements, key=f])

There are many ways of writing this. Here are a few. It’s important to keep straight in your
head which variables contain scores and which contain elements. Any of these solutions
would have been fine; we wouldn’t expect most people to get the last one.

3.8.5 Practice problem: OOP


The following definitions have been entered into a Python shell:

class A:
yours = 0
def __init__(self, inp):
self.yours = inp
def give(self, amt):
self.yours = self.yours + amt
return self.yours
def howmuch(self):
return (A.yours, self.yours)

class B(A):
yours = 0
def give(self, amt):
B.yours = B.yours + amt
return A.howmuch(self)
def take(self, amt):
self.yours = self.yours - amt
return self.yours
def howmuch(self):
return (B.yours, A.yours, self.yours)
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 102

Write the values of the following expressions. Write None when there is no value; write Error
when an error results and explain briefly why it is an error. Assume that these expressions are
evaluated one after another (all of the left column first, then right column).
test = A(5) test = B(5)

test.take(2) test.take(2)

test.give(6) test.give(6)

test.howmuch() test.howmuch()

• None: This makes test be equal to a new instance of A, with attribute yours equal to 5.
• Error: Instances of A don’t have a take method.
• 11: This adds 6 to the value of test.yours and returns its value.
• (0, 11): At this point, we haven’t changed the class attribute A.yours, so it still has
value 0.
• None: This makes test be equal to a new instance of B; it inherits the init method from A,
so at this point test.yours is equal to 5.
• 3: Now, since test is an instance of B, the method test.take is defined; it changes
test.yours to be 3 and returns its value.
• (0, 3): Note that this refers to the give method defined in class B. So, first, the amount is
used to increment B.yours to have value 6. Then, we return A.yours and test.yours,
which are both unchanged.
• (6, 0, 3): This just returns the current values of all three yours attributes, one in each
class definition as well as test.yours.

3.8.6 Practice problem: The Best and the Brightest


Here are some class definitions, meant to represent a collection of students making up the student
body of a prestigious university.

class Student:
def __init__(self, name, iq, salary, height):
self.name = name
self.iq = iq
self.salary = salary
self.height = height

class StudentBody:
def __init__(self):
self.students = []
def addStudent(self, student):
self.students.append(student)
def nameOfSmartest(self):
# code will go here
def funOfBest(self, fun, feature):
# code will go here

The StudentBody class is missing the code for two methods:


Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 103

• nameOfSmartest: returns the name attribute of the student with the highest IQ
• funOfBest: takes a procedure fun and a procedure feature as input, and returns the result
of applying procedure fun to the student for whom the procedure feature yields the highest
value
For the first two problems below, assume that these methods have been implemented.
Here is a student body:

jody = Student(’Jody’, 100, 100000, 80)


chris = Student(’Chris’, 150, 40000, 62)
dana = Student(’Dana’, 120, 2000, 70)
aardvarkU = StudentBody()
aardvarkU.addStudent(jody)
aardvarkU.addStudent(chris)
aardvarkU.addStudent(dana)

1. What is the result of evaluating aardvarkU.nameOfSmartest()?

’Chris’

2. Write a Python expression that will compute the name of the person who has the greatest
value of IQ + height in aardvarkU (not just for the example student body above). You can
define additional procedures if you need them.

aardvarkU.funOfBest(lambda x: x.name, lambda x: x.iq + x.height)

The second lambda expression specifies how to evaluate an element (that is, to sum its iq
and height attributes) and the first lambda expression says what function to apply to the
element with the highest score.

3. Implement the nameOfSmartest method. For full credit, use util.argmax (defined below)
or the funOfBest method.
If l is a list of items and f is a procedure that maps an item into a numeric score, then
util.argmax(l, f) returns the element of l that has the highest score.

def nameOfSmartest(self):
return util.argmax(self.students, lambda x: x.iq).name

# or

def nameOfSmartest(self):
return funOfBest(lambda x: x.name, lambda x: x.iq)

Note that in the first solution, the expression util.argmax(self.students, lambda x:


x.iq) returns the object with the highest iq attribute; then we return the name attribute of
that object.
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 104

4. Implement the funOfBest method. For full credit, use util.argmax.

def funOfBest(self, fun, feature):


return fun(util.argmax(self.students, feature))

3.8.7 Practice Problem: A Library with Class


Let’s build a class to represent a library; let’s call it Library. In this problem, we’ll deal with
some standard types of objects:
• A book is represented as a string – its title.
• A patron (person who uses the library) is represented as a string – his/her name.
• A date is represented by an integer – the number of days since the library opened.

The class should have an attribute called dailyFine that starts out as 0.25. The class should
have the following methods:
• __init__: takes a list of books and initializes the library.
• checkOut: is given a book, a patron and a date on which the book is being checked out and it
records this. Each book can be kept for 7 days before it becomes overdue, i.e. if checked out on
day x, it becomes due on day x + 7 and it will be considered overdue by one day on day x + 8.
It returns None.
• checkIn: is given a book and a date on which the book is being returned and it updates the
records. It returns a number representing the fine due if the book is overdue and 0.0 otherwise.
The fine is the number of days overdue times the value of the dailyFine attribute.
• overdueBooks: is given a patron and a date and returns the list of books which that patron has
checked out which are overdue at the given date.

Here is an example of the operation of the library:

>>> lib = Library([’a’, ’b’, ’c’, ’d’, ’e’, ’f’])


>>> lib.checkOut(’a’, ’T’, 1)
>>> lib.checkOut(’c’, ’T’, 1)
>>> lib.checkOut(’e’, ’T’, 10)
>>> lib.overdueBooks(’T’, 13)
[’a’, ’c’]
>>> lib.checkIn(’a’, 13)
1.25
>>> lib.checkIn(’c’, 18)
2.50
>>> lib.checkIn(’e’, 18)
0.25

In the boxes below, define the Library class as described above. Above each answer box we
repeat the specification for each of the attributes and methods given above. Make sure that you
enter complete definitions in the boxes, including complete class and def statements.
Use a dictionary to store the contents of the library. Do not repeat code if at all possible. You can
assume that all the operations are legal, for example, all books checked out are in the library and
books checked in have been previously checked out.
Class definition:
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 105

Include both the start of the class definition and the method definition for __init__ in this first
answer box.
The class should have an attribute called dailyFine that starts out as 0.25.
__init__: takes a list of books and initializes the library.

class Library:
dailyFine = 0.25
def __init__(self, books):
self.shelf = {}
for book in books:
self.shelf[book] = (None, None) # (patron, dueDate)

The crucial part here is deciding what information to store. Here, for each book, we store a
tuple of who has checked it out, and when it is due.

checkOut: is given a book, a patron and a date on which the book is being checked out and it
records this. Each book can be kept for 7 days before it becomes overdue, i.e. if checked out on
day x, it becomes due on day x + 7 and it will be considered overdue by one day on day x + 8. It
returns None.

def checkOut(self, book, patron, date):


self.shelf[book] = (patron, date+7)

checkIn: is given a book and a date on which the book is being returned and it updates the
records. It returns a number representing the fine due if the book is overdue and 0.0 otherwise.
The fine is the number of days overdue times the value of the dailyFine attribute.

def checkIn(self, book, date):


patron, due = self.shelf[book]
self.shelf[book] = (None, None)
return max(0.0, (date - due))*self.dailyFine

Note the destructuring assignment in the first line. It’s not crucial, but it’s nice style, and
keeps us from having to refer to components like self.shelf[book][1], which are ugly,
long, and hard to get right. Instead of using a max, you could use an if statement to handle
the case of the book being overdue or not, but this is more compact and pretty clear.

overdueBooks: is given a patron and a date and returns the list of books which that patron has
checked out which are overdue at the given date.
Chapter 3 Programs and Data 6.01— Spring 2011— February 1, 2011 106

def overdueBooks(self, patron, date):


overdue = []
for book in self.shelf:
p, d = self.shelf[book]
if p and d and p == patron and date > d:
overdue.append(book)
return overdue

It’s not really necessary to check to be sure that p and d are not None, because p == patron
will only be true for a real patron, and if the patron is not None then d won’t be either. But
this code makes it clear that we’re only interested in books that have been checked out.

Define a new class called LibraryGrace that behaves just like the Library class except that it
provides a grace period (some number of days after the actual due date) before fines start being
accumulated. The number of days in the grace period is specified when an instance is created.
See the example below.

>>> lib = LibraryGrace(2, [’a’, ’b’, ’c’, ’d’, ’e’, ’f’])


>>> lib.checkOut(’a’, ’T’, 1)
>>> lib.checkIn(’a’, 13)
0.75

Write the complete class definition for LibraryGrace. To get full credit you should not repeat
any code that is already in the implementation of Library, in particular, you should not need to
repeat the computation of the fine.
Class definition:
class LibraryGrace(Library):
def __init__(self, grace, books):
self.grace = grace
Library.__init__(self, books)
def checkIn(self, book, date):
return Library.checkIn(self, book, date - self.grace)

The crucial things here are: remembering to call the __init__ method of the parent class
and doing something to handle the grace period. In this example, when we check a book
back in, we pretend we’re actually checking it in on an earlier date. Alternatively, we could
have changed the checkout method to pretend that the checkout was actually happening in
the future.
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 107

Chapter 4
State Machines

State machines are a method of modeling systems whose output depends on the entire history
of their inputs, and not just on the most recent input. Compared to purely functional systems,
in which the output is purely determined by the input, state machines have a performance that
is determined by its history. State machines can be used to model a wide variety of systems,
including:
• user interfaces, with typed input, mouse clicks, etc.;
• conversations, in which, for example, the meaning of a word “it” depends on the history of
things that have been said;
• the state of a spacecraft, including which valves are open and closed, the levels of fuel and
oxygen, etc.; and
• the sequential patterns in DNA and what they mean.
State machine models can either be continuous time or discrete time. In continuous time models,
we typically assume continuous spaces for the range of values of inputs and outputs, and use
differential equations to describe the system’s dynamics. This is an interesting and important ap-
proach, but it is hard to use it to describe the desired behavior of our robots, for example. The
loop of reading sensors, computing, and generating an output is inherently discrete and too slow
to be well-modeled as a continuous-time process. Also, our control policies are often highly non-
linear and discontinuous. So, in this class, we will concentrate on discrete-time models, meaning
models whose inputs and outputs are determined at specific increments of time, and which are
synchronized to those specific time samples. Furthermore, in this chapter, we will make no as-
sumptions about the form of the dependence of the output on the time-history of inputs; it can be
an arbitrary function.
Generally speaking, we can think of the job of an embedded system as performing a transduction
from a stream (infinite sequence) of input values to a stream of output values. In order to specify
the behavior of a system whose output depends on the history of its inputs mathematically, you
could think of trying to specify a mapping from i1 , . . . , it (sequences of previous inputs) to ot
(current output), but that could become very complicated to specify or execute as the history gets
longer. In the state-machine approach, we try to find some set of states of the system, which
capture the essential properties of the history of the inputs and are used to determine the current
output of the system as well as its next state. It is an interesting and sometimes difficult modeling
problem to find a good state-machine model with the right set of states; in this chapter we will
explore how the ideas of PCAP can aid us in designing useful models.
One thing that is particularly interesting and important about state machine models is how many
ways we can use them. In this class, we will use them in three fairly different ways:
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 108

1. Synthetically: State machines can specify a “program” for a robot or other system embedded
in the world, with inputs being sensor readings and outputs being control commands.
2. Analytically: State machines can describe the behavior of the combination of a control system
and the environment it is controlling; the input is generally a simple command to the entire
system, and the output is some simple measure of the state of the system. The goal here is
to analyze global properties of the coupled system, like whether it will converge to a steady
state, or will oscillate, or will diverge.
3. Predictively: State machines can describe the way the environment works, for example, where
I will end up if I drive down some road from some intersection. In this case, the inputs are
control commands and the outputs are states of the external world. Such a model can be
used to plan trajectories through the space of the external world to reach desirable states, by
considering different courses of action and using the model to predict their results.
We will develop a single formalism, and an encoding of that formalism in Python classes, that
will serve all three of these purposes.
Our strategy for building very complex state machines will be, abstractly, the same strategy we
use to build any kind of complex machine. We will define a set of primitive machines (in this
case, an infinite class of primitive machines) and then a collection of combinators that allow us
to put primitive machines together to make more complex machines, which can themselves be
abstracted and combined to make more complex machines.

4.1 Primitive state machines


We can specify a transducer (a process that takes as input a sequence of vaues which serve as
inputs to the state machine, and returns as ouput the set of outputs of the machine for each input)
as a state machine (SM) by specifying:
• a set of states, S,
• a set of inputs, I, also called the input vocabulary,
• a set of outputs, O, also called the output vocabulary,
• a next-state function, n(it , st ) 7→ st+1 , that maps the input at time t and the state at time t to
the state at time t + 1,
• an output function, o(it , st ) 7→ ot , that maps the input at time t and the state at time t to the
output at time t; and
• an initial state, s0 , which is the state at time 0.
Here are a few state machines, to give you an idea of the kind of systems we are considering.
• A tick-tock machine that generates the sequence 1, 0, 1, 0, . . . is a finite-state machine that ig-
nores its input.
• The controller for a digital watch is a more complicated finite-state machine: it transduces a
sequence of inputs (combination of button presses) into a sequence of outputs (combinations
of segments illuminated in the display).
• The controller for a bank of elevators in a large office building: it transduces the current set
of buttons being pressed and sensors in the elevators (for position, open doors, etc.) into
commands to the elevators to move up or down, and open or close their doors.
The very simplest kind of state machine is a pure function: if the machine has no state, and the
output function is purely a function of the input, for example, ot = it + 1, then we have an
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 109

immediate functional relationship between inputs and outputs on the same time step. Another
simple class of SMs are finite-state machines, for which the set of possible states is finite. The
elevator controller can be thought of as a finite-state machine, if elevators are modeled as being
only at one floor or another (or possibly between floors); but if the controller models the exact
position of the elevator (for the purpose of stopping smoothly at each floor, for example), then it
will be most easily expressed using real numbers for the state (though any real instantiation of it
can ultimately only have finite precision). A different, large class of SMs are describable as linear,
time-invariant (LTI) systems. We will discuss these in detail chapter ??.

4.1.1 Examples
Let’s look at several examples of state machines, with complete formal definitions.

4.1.1.1 Language acceptor


Here is a finite-state machine whose output is true if the input string adheres to a simple pattern,
and false otherwise. In this case, the pattern has to be a, b, c, a, b, c, a, b, c, . . ..
It uses the states 0, 1, and 2 to stand for the situations in which it is expecting an a, b, and c,
respectively; and it uses state 3 for the situation in which it has seen an input that was not the one
that was expected. Once the machine goes to state 3 (sometimes called a rejecting state), it never
exits that state.

S = {0, 1, 2, 3}
I = {a, b, c}
O = {true, false}

 1 if s = 0, i = a

2 if s = 1, i = b
n(s, i) =

 0 if s = 2, i = c
3 otherwise

false if n(s, i) = 3
o(s, i) =
true otherwise
s0 = 0

Figure 4.1 shows a state transition diagram for this state machine. Each circle represents a state.
The arcs connecting the circles represent possible transitions the machine can make; the arcs are
labeled with a pair i, o, which means that if the machine is in the state denoted by a circle with
label s, and gets an input i, then the arc points to the next state, n(s, i) and the output generated
o(s, i) = o. Some arcs have several labels, indicating that there are many different inputs that will
cause the same transition. Arcs can only be traversed in the direction of the arrow.
For a state transition diagram to be complete, there must be an arrow emerging from each state
for each possible input i (if the next state is the same for some inputs, then we draw the graph
more compactly by using a single arrow with multiple labels, as you will see below).
We will use tables like the following one to examine the evolution of a state machine:
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 110

c / True

0 a / True 1 b / True 2

a / False
c / False
b / False a / False
c / False b / False
3

a / False
b / False
c / False

Figure 4.1 State transition diagram for language acceptor.

time 0 1 2 ...

input i0 i1 i2 ...
state s0 s1 s2 ...
output o1 o2 o3 ...

For each column in the table, given the current input value and state we can use the output
function o to determine the output in that column; and we use the n function applied to that
input and state value to determine the state in the next column. Thus, just knowing the input
sequence and s0 , and the next-state and output functions of the machine will allow you to fill in
the rest of the table.
For example, here is the state of the machine at the initial time point:

time 0 1 2 ...

input i0 ...
state s0 ...
output ...

Using our knowledge of the next state function n, we have:

time 0 1 2 ...

input i0 ...
state s0 s1 ...
output ...
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 111

and using our knowledge of the output function o, we have at the next input value

time 0 1 2 ...

input i0 i1 ...
state s0 s1 ...
output o1 ...

This completes one cycle of the statement machine, and we can now repeat the process.
Here is a table showing what the language acceptor machine does with input sequence (’a’, ’b’,
’c’, ’a’, ’c’, ’a’, ’b’):

time 0 1 2 3 4 5 6 7

input ’a’ ’b’ ’c’ ’a’ ’c’ ’a’ ’b’


state 0 1 2 0 1 3 3 3
output True True True True False False False

The output sequence is (True, True, True, True, False, False, False).
Clearly we don’t want to analyze a system by considering all input sequences, but this table helps
us understand the state transitions of the system model.

To learn more: Finite-state machine language acceptors can be built for a class of patterns
called regular languages. There are many more complex patterns (such as
the set of strings with equal numbers of 1’s and 0’s) that cannot be rec-
ognized by finite-state machines, but can be recognized by a specialized
kind of infinite-state machine called a stack machine. To learn more about
these fundamental ideas in computability theory, start with the Wikipedia
article on Computability_theory_(computer_science)

4.1.1.2 Up and down counter


This machine can count up and down; its state space is the countably infinite set of integers. It
starts in state 0. Now, if it gets input u, it goes to state 1; if it gets u again, it goes to state 2. If it
gets d, it goes back down to 1, and so on. For this machine, the output is always the same as the
next state.

S = integers
I = {u, d}
O = integers

s + 1 if i = u
n(s, i) =
s − 1 if i = d
o(s, i) = n(s, i)
s0 = 0
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 112

Here is a table showing what the up and down counter does with input sequence u, u, u, d, d, u:

time 0 1 2 3 4 5 6

input u u u d d u
state 0 1 2 3 2 1 2
output 1 2 3 2 1 2

The output sequence is 1, 2, 3, 2, 1, 2.

4.1.1.3 Delay
An even simpler machine just takes the input and passes it through to the output, but with one
step of delay, so the kth element of the input sequence will be the k + 1st element of the output
sequence. Here is the formal machine definition:
S = anything
I = anything
O = anything
n(s, i) = i
o(s, i) = s
s0 = 0

Given an input sequence i0 , i1 , i2 , . . ., this machine will produce an output sequence


0, i0 , i1 , i2 , . . .. The initial 0 comes because it has to be able to produce an output before it
has even seen an input, and that output is produced based on the initial state, which is 0. This
very simple building block will come in handy for us later on.
Here is a table showing what the delay machine does with input sequence 3, 1, 2, 5, 9:

time 0 1 2 3 4 5

input 3 1 2 5 9
state 0 3 1 2 5 9
output 0 3 1 2 5

The output sequence is 0, 3, 1, 2, 5.

4.1.1.4 Accumulator
Here is a machine whose output is the sum of all the inputs it has ever seen.
S = numbers
I = numbers
O = numbers
n(s, i) = s + i
o(s, i) = n(s, i)
s0 = 0
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 113

Here is a table showing what the accumulator does with input sequence 100, −3, 4, −123, 10:

time 0 1 2 3 4 5

input 100 -3 4 -123 10


state 0 100 97 101 -22 -12
output 100 97 101 -22 -12

4.1.1.5 Average2
Here is a machine whose output is the average of the current input and the previous input. It
stores its previous input as its state.

S = numbers
I = numbers
O = numbers
n(s, i) = i
o(s, i) = (s + i)/2
s0 = 0

Here is a table showing what the average2 machine does with input sequence 100, −3, 4, −123, 10:

time 0 1 2 3 4 5

input 100 -3 4 -123 10


state 0 100 -3 4 -123 10
output 50 48.5 0.5 -59.5 -56.5

4.1.2 State machines in Python


Now, it is time to make computational implementations of state machine models. In this section
we will build up some basic Python infrastructure to make it straightforward to define primitive
machines; in later sections we will see how to combine primitive machines into more complex
structures.
We will use Python’s object-oriented facilities to make this convenient. We have an abstract class,
SM, which will be the superclass for all of the particular state machine classes we define. It does
not make sense to make an instance of SM, because it does not actually specify the behavior of
the machine; it just provides some utility methods. To specify a new type of state machine, you
define a new class that has SM as a superclass.
In any subclass of SM, it is crucial to define an attribute startState, which specifies the initial
state of the machine, and a method getNextValues which takes the state at time t and the input
at time t as input, and returns the state at time t + 1 and the output at time t. This is a choice
that we have made as designers of our state machine model; we will rely on these two pieces of
information in our underlying infrastructure for simulating state machines, as we will see shortly.
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 114

Here, for example, is the Python code for an accumulator state machine, which implements the
definition given in section 4.1.1.4. 29

class Accumulator(SM):
startState = 0
def getNextValues(self, state, inp):
return (state + inp, state + inp)

It is important to note that getNextValues does not change the state of the machine, in other
words, it does not mutate a state variable. Its job is to be a pure function: to answer the question
of what the next state and output would be if the current state were state and the current input
were inp. We will use the getNextValues methods of state machines later in the class to make
plans by considering alternative courses of action, so they must not have any side effects. As we
noted, this is our choice as designers of the state machine infrastructure We could have chosen to
implement things differently, however this choice will prove to be very useful. Thus, in all our
state machines, the function getNextValues will capture the transition from input and state to
output and state, without mutating any stored state values.
To run a state machine, we make an instance of the appropriate state-machine class, call its start
method (a built in method we will see shortly) to set the state to the starting state, and then ask
it to take steps; each step consists of generating an output value (which is printed) and updating
the state to the next state, based on the input. The abstract superclass SM defines the start and
step methods. These methods are in charge of actually initializing and then changing the state
of the machine as it is being executed. They do this by calling the getNextValues method for
the class to which this instance belongs. The start method takes no arguments (but, even so, we
have to put parentheses after it, indicating that we want to call the method, not to do something
with the method itself); the step method takes one argument, which is the input to the machine
on the next step. So, here is a run of the accumulator, in which we feed it inputs 3, 4, and -2:

>>> a = Accumulator()
>>> a.start()
>>> a.step(3)
3
>>> a.step(4)
7
>>> a.step(-2)
5

The class SM specifies how state machines work in general; the class Accumulator specifies how
accumulator machines work in general; and the instance a is a particular machine with a particu-
lar current state. We can make another instance of accumulator:

>>> b = Accumulator()
>>> b.start()
>>> b.step(10)
10
>>> b.state
10

29
Throughout this code, we use inp, instead of input, which would be clearer. The reason is that the name input is
used by Python as a function. Although it is legal to re-use it as the name of an argument to a procedure, doing so is a
source of bugs that are hard to find (if, for instance, you misspell the name input in the argument list, your references
to input later in the procedure will be legal, but will return the built-in function.)
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 115

>>> a.state
5

Now, we have two accumulators, a, and b, which remember, individually, in what states they
exist. Figure 4.2 shows the class and instance structures that will be in place after creating these
two accumulators.

start
step
transduce
global run

SM
Accumulator
a startState 0
b getNextValues

state 10 state 5

Figure 4.2 Classes and instances for an Accumulator SM. All of the dots represent procedure
objects.

4.1.2.1 Defining a type of SM


Let’s go back to the definition of the Accumulator class, and study it piece by piece.
First, we define the class and say that it is a subclass of SM:

class Accumulator(SM):

Next, we define an attribute of the class, startState, which is the starting state of the machine.
In this case, our accumulator starts up with the value 0.

startState = 0

Note that startState is required by the underlying SM class, so we must either define it in our
subclass definition or use a default value from the SM superclass.
The next method defines both the next-state function and the output function, by taking the cur-
rent state and input as arguments and returning a tuple containing both the next state and the
output.
For our accumulator, the next state is just the sum of the previous state and the input; and the
output is the same thing:

def getNextValues(self, state, inp):


return (state + inp, state + inp)

It is crucial that getNextValues be a pure function; that is, that it not change the state of the
object (by assigning to any attributes of self). It must simply compute the necessary values and
return them. We do not promise anything about how many times this method will be called and
in what circumstances.
Sometimes, it is convenient to arrange it so that the class really defines a range of machines with
slightly different behavior, which depends on some parameters that we set at the time we create
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 116

an instance. So, for example, if we wanted to specify the initial value for an accumulator at the
time the machine is created, we could add an __init__ method 30 to our class, which takes an
initial value as a parameter and uses it to set an attribute called startState of the instance. 31

class Accumulator(SM):
def __init__(self, initialValue):
self.startState = initialValue
def getNextValues(self, state, inp):
return state + inp, state + inp

Now we can make an accumulator and run it like this:

>>> c = Accumulator(100)
>>> c.start()
>>> c.step(20)
120
>>> c.step(2)
122

4.1.2.2 The SM Class


The SM class contains generally useful methods that apply to all state machines. A state machine
is an instance of any subclass of SM, that has defined the attribute startState and the method
getNextValues, as we did for the Accumulator class. Here we examine these methods in more
detail.

Running a machine
The first group of methods allows us to run a state machine. To run a machine is to provide it with
a sequence of inputs and then sequentially go forward, computing the next state and generating
the next output, as if we were filling in a state table.
To run a machine, we have to start by calling the start method. All it does is create an attribute
of the instance, called state, and assign to it the value of the startState attribute. It is crucial
that we have both of these attributes: if we were to just modify startState, then if we wanted
to run this machine again, we would have permanently forgotten what the starting state should
be. Note that state becomes a repository for the state of this instance; however we should not
mutate it directly. This variable becomes the internal representation of state for each instance of
this class.

class SM:
def start(self):
self.state = self.startState

Once it has started, we can ask it to take a step, using the step method, which, given an input,
computes the output and updates the internal state of the machine, and returns the output value.

30
Remember that the __init__ method is a special feature of the Python object-oriented system, which is called when-
ever an instance of the associated class is created.
31
Note that in the original version of Accumulator, startState was an attribute of the class, since it was the same for
every instance of the class; now that we want it to be different for different instances, we need startState to be an
attribute of the instance rather than the class, which is why we assign it in the __init__ method, which modifies the
already-created instance.
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 117

def step(self, inp):


(s, o) = self.getNextValues(self.state, inp)
self.state = s
return o

To run a machine on a whole sequence of input values, we can use the transduce method, which
will return the sequence of output values that results from feeding the elements of the list inputs
into the machine in order.

def transduce(self, inputs):


self.start()
return [self.step(inp) for inp in inputs]

Here are the results of running transduce on our accumulator machine. We run it twice, first
with a simple call that does not generate any debugging information, and simply returns the
result. The second time, we ask it to be verbose, resulting in a print-out of what is happening on
the intermediate steps. 32

a = Accumulator()
>>> a.transduce([100, -3, 4, -123, 10])
[100, 97, 101, -22, -12]
>>> a.transduce([100, -3, 4, -123, 10], verbose = True)
Start state: 0
In: 100 Out: 100 Next State: 100
In: -3 Out: 97 Next State: 97
In: 4 Out: 101 Next State: 101
In: -123 Out: -22 Next State: -22
In: 10 Out: -12 Next State: -12
[100, 97, 101, -22, -12]

Some machines do not take any inputs; in that case, we can simply call the SM run method, which
is equivalent to doing transduce on an input sequence of [None, None, None, ...].

def run(self, n = 10):


return self.transduce([None]*n)

4.1.2.2.1 Default methods


This section is optional.
In order to make the specifications for the simplest machine types as short as possible, we have
also provided a set of default methods in the SM class. These default methods say that, unless they
are overridden in a subclass, as they were when we defined Accumulator, we will assume that a
machine starts in state None and that its output is the same as its next state.

startState = None
def getNextValues(self, state, inp):
nextState = self.getNextState(state, inp)
return (nextState, nextState)

32
In fact, the second machine trace, and all the others in this section were generated with a call like:

>>> a.transduce([100, -3, 4, -123, 10], verbose = True, compact = True)

See the Infrastructure Guide for details on different debugging options. To simplify the code examples we show in
these notes, we have omitted parts of the code that are responsible for debugging printouts.
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 118

Because these methods are provided in SM, we can define, for example, a state machine whose out-
put is always k times its input, with this simple class definition, which defines a getNextState
procedure that simply returns a value that is treated as both the next state and the output.

class Gain(SM):
def __init__(self, k):
self.k = k
def getNextState(self, state, inp):
return inp * self.k

We can use this class as follows:

>>> g = Gain(3)
>>> g.transduce([1.1, -2, 100, 5])
[3.3000000000000003, -6, 300, 15]

The parameter k is specified at the time the instance is created. Then, the output of the machine
is always just k times the input.
We can also use this strategy to write the Accumulator class even more succinctly:

class Accumulator(SM):
startState = 0
def getNextState(self, state, inp):
return state + inp

The output of the getNextState method will be treated both as the output and the next state of
the machine, because the inherited getNextValues function uses it to compute both values.

4.1.2.3 More examples


Here are Python versions of the rest of the machines we introduced in the first section.

Language acceptor
Here is a Python class for a machine that “accepts” the language that is any prefix of the infinite
sequence [’a’, ’b’, ’c’, ’a’, ’b’, ’c’, ....].

class ABC(SM):
startState = 0
def getNextValues(self, state, inp):
if state == 0 and inp == ’a’:
return (1, True)
elif state == 1 and inp == ’b’:
return (2, True)
elif state == 2 and inp == ’c’:
return (0, True)
else:
return (3, False)

It behaves as we would expect. As soon as it sees a character that deviates from the desired
sequence, the output is False, and it will remain False for ever after.
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 119

>>> abc = ABC()


>>> abc.transduce([’a’,’a’,’a’], verbose = True)
Start state: 0
In: a Out: True Next State: 1
In: a Out: False Next State: 3
In: a Out: False Next State: 3
[True, False, False]

>>> abc.transduce([’a’, ’b’, ’c’, ’a’, ’c’, ’a’, ’b’], verbose = True)
Start state: 0
In: a Out: True Next State: 1
In: b Out: True Next State: 2
In: c Out: True Next State: 0
In: a Out: True Next State: 1
In: c Out: False Next State: 3
In: a Out: False Next State: 3
In: b Out: False Next State: 3
[True, True, True, True, False, False, False]

Count up and down


This is a direct translation of the machine defined in section 4.1.1.2.

class UpDown(SM):
startState = 0
def getNextState(self, state, inp):
if inp == ’u’:
return state + 1
else:
return state - 1

We take advantage of the default getNextValues method to make the output the same as the
next state.

>>> ud = UpDown()
>>> ud.transduce([’u’,’u’,’u’,’d’,’d’,’u’], verbose = True)
Start state: 0
In: u Out: 1 Next State: 1
In: u Out: 2 Next State: 2
In: u Out: 3 Next State: 3
In: d Out: 2 Next State: 2
In: d Out: 1 Next State: 1
In: u Out: 2 Next State: 2
[1, 2, 3, 2, 1, 2]

Delay
In order to make a machine that delays its input stream by one time step, we have to specify what
the first output should be. We do this by passing the parameter, v0, into the __init__ method
of the Delay class. The state of a Delay machine is just the input from the previous step, and the
output is the state (which is, therefore, the input from the previous time step).

class Delay(SM):
def __init__(self, v0):
self.startState = v0
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 120

def getNextValues(self, state, inp):


return (inp, state)

>>> d = Delay(7)
>>> d.transduce([3, 1, 2, 5, 9], verbose = True)
Start state: 7
In: 3 Out: 7 Next State: 3
In: 1 Out: 3 Next State: 1
In: 2 Out: 1 Next State: 2
In: 5 Out: 2 Next State: 5
In: 9 Out: 5 Next State: 9
[7, 3, 1, 2, 5]

>>> d100 = Delay(100)


>>> d100.transduce([3, 1, 2, 5, 9], verbose = True)
Start state: 100
In: 3 Out: 100 Next State: 3
In: 1 Out: 3 Next State: 1
In: 2 Out: 1 Next State: 2
In: 5 Out: 2 Next State: 5
In: 9 Out: 5 Next State: 9
[100, 3, 1, 2, 5]

We will use this machine so frequently that we put its definition in the sm module (file), along
with the class.

R
We can use R as another name for the Delay class of state machines. It will be an important
primitive in a compositional system of linear time-invariant systems, which we explore in the
next chapter.

Average2
Here is a state machine whose output at time t is the average of the input values from times t − 1
and t.

class Average2(SM):
startState = 0
def getNextValues(self, state, inp):
return (inp, (inp + state) / 2.0)

It needs to remember the previous input, so the next state is equal to the input. The output is the
average of the current input and the state (because the state is the previous input).

>>> a2 = Average2()
>>> a2.transduce([10, 5, 2, 10], verbose = True, compact = True)
Start state: 0
In: 10 Out: 5.0 Next State: 10
In: 5 Out: 7.5 Next State: 5
In: 2 Out: 3.5 Next State: 2
In: 10 Out: 6.0 Next State: 10
[5.0, 7.5, 3.5, 6.0]
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 121

Sum of last three inputs


Here is an example of a state machine where the state is actually a list of values. Generally speak-
ing, the state can be anything (a dictionary, an array, a list); but it is important to be sure that
the getNextValues method does not make direct changes to components of the state, instead
returning a new copy of the state with appropriate changes. We may make several calls to the
getNextValues function on one step (or, later in our work, call the getNextValues function
with several different inputs to see what would happen under different choices); these function
calls are made to find out a value of the next state, but if they actually change the state, then the
same call with the same arguments may return a different value the next time.
This machine generates as output at time t the sum of it−2 , it−1 and it ; that is, of the last three
inputs. In order to do this, it has to remember the values of two previous inputs; so the state is
a pair of numbers. We have defined it so that the initial state is (0, 0). The getNextValues
method gets rid of the oldest value that it has been remembering, and remembers the current
input as part of the state; the output is the sum of the current input with the two old inputs that
are stored in the state. Note that the first line of the getNextValues procedure is a structured
assignment (see section 3.3).

class SumLast3 (SM):


startState = (0, 0)
def getNextValues(self, state, inp):
(previousPreviousInput, previousInput) = state
return ((previousInput, inp),
previousPreviousInput + previousInput + inp)

>>> sl3 = SumLast3()


>>> sl3.transduce([2, 1, 3, 4, 10, 1, 2, 1, 5], verbose = True)
Start state: (0, 0)
In: 2 Out: 2 Next State: (0, 2)
In: 1 Out: 3 Next State: (2, 1)
In: 3 Out: 6 Next State: (1, 3)
In: 4 Out: 8 Next State: (3, 4)
In: 10 Out: 17 Next State: (4, 10)
In: 1 Out: 15 Next State: (10, 1)
In: 2 Out: 13 Next State: (1, 2)
In: 1 Out: 4 Next State: (2, 1)
In: 5 Out: 8 Next State: (1, 5)
[2, 3, 6, 8, 17, 15, 13, 4, 8]

Selector
A simple functional machine that is very useful is the Select machine. You can make many
different versions of this, but the simplest one takes an input that is a stream of lists or tuples of
several values (or structures of values) and generates the stream made up only of the kth elements
of the input values. Which particular component this machine is going to select is determined by
the value k, which is passed in at the time the machine instance is initialized.

class Select (SM):


def __init__(self, k):
self.k = k
def getNextState(self, state, inp):
return inp[self.k]
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 122

4.1.3 Simple parking gate controller


As one more demonstration, here is a simple example of a finite-state controller for a gate leading
out of a parking lot.
The gate has three sensors:
• gatePosition has one of three values ’top’, ’middle’, ’bottom’, signi-
fying the position of the arm of the parking gate.
• carAtGate is True if a car is waiting to come through the gate and False
otherwise.
• carJustExited is True if a car has just passed through the gate; it is true
for only one step before resetting to False.

Pause to try 4.1. How many possible inputs are there?

A: 12.

The gate has three possible outputs (think of them as controls to the motor for the gate arm):
’raise’, ’lower’, and ’nop’. (Nop means “no operation.”)
Roughly, here is what the gate needs to do:
• If a car wants to come through, the gate needs to raise the arm until it is at the top position.
• Once the gate is at the top position, it has to stay there until the car has driven through the
gate.
• After the car has driven through the gate needs to lower the arm until it reaches the bottom
position.
So, we have designed a simple finite-state controller with a state transition diagram as shown
in figure 4.3. The machine has four possible states: ’waiting’ (for a car to arrive at the gate),
’raising’ (the arm), ’raised’ (the arm is at the top position and we’re waiting for the car to
drive through the gate), and ’lowering’ (the arm). To keep the figure from being too cluttered,
we do not label each arc with every possible input that would cause that transition to be made:
instead, we give a condition (think of it as a Boolean expression) that, if true, would cause the
transition to be followed. The conditions on the arcs leading out of each state cover all the possible
inputs, so the machine remains completely well specified.
Here is a simple state machine that implements the controller for the parking gate. For com-
pactness, the getNextValues method starts by determining the next state of the gate. Then,
depending on the next state, the generateOutput method selects the appropriate output.

class SimpleParkingGate (SM):


startState = ’waiting’

def generateOutput(self, state):


if state == ’raising’:
return ’raise’
elif state == ’lowering’:
return ’lower’
else:
return ’nop’
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 123

not top / raise

raising
top / nop
carAtGate / raise

not carAtGate / nop waiting raised not carJustExited / nop

bottom / nop carJustExited / lower

lowering

not bottom / lower

Figure 4.3 State transition diagram for parking gate controller.

def getNextValues(self, state, inp):


(gatePosition, carAtGate, carJustExited) = inp
if state == ’waiting’ and carAtGate:
nextState = ’raising’
elif state == ’raising’ and gatePosition == ’top’:
nextState = ’raised’
elif state == ’raised’ and carJustExited:
nextState = ’lowering’
elif state == ’lowering’ and gatePosition == ’bottom’:
nextState = ’waiting’
else:
nextState = state
return (nextState, self.generateOutput(nextState))

In the situations where the state does not change (that is, when the arcs lead back to the same
state in the diagram), we do not explicitly specify the next state in the code: instead, we cover it in
the else clause, saying that unless otherwise specified, the state stays the same. So, for example,
if the state is raising but the gatePosition is not yet top, then the state simply stays raising
until the top is reached.

>>> spg = SimpleParkingGate()


>>> spg.transduce(testInput, verbose = True)
Start state: waiting
In: (’bottom’, False, False) Out: nop Next State: waiting
In: (’bottom’, True, False) Out: raise Next State: raising
In: (’bottom’, True, False) Out: raise Next State: raising
In: (’middle’, True, False) Out: raise Next State: raising
In: (’middle’, True, False) Out: raise Next State: raising
In: (’middle’, True, False) Out: raise Next State: raising
In: (’top’, True, False) Out: nop Next State: raised
In: (’top’, True, False) Out: nop Next State: raised
In: (’top’, True, False) Out: nop Next State: raised
In: (’top’, True, True) Out: lower Next State: lowering
In: (’top’, True, True) Out: lower Next State: lowering
In: (’top’, True, False) Out: lower Next State: lowering
In: (’middle’, True, False) Out: lower Next State: lowering
In: (’middle’, True, False) Out: lower Next State: lowering
In: (’middle’, True, False) Out: lower Next State: lowering
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 124

In: (’bottom’, True, False) Out: nop Next State: waiting


In: (’bottom’, True, False) Out: raise Next State: raising
[’nop’, ’raise’, ’raise’, ’raise’, ’raise’, ’raise’, ’nop’, ’nop’, ’nop’, ’lower’, ’lower’,
’lower’, ’lower’, ’lower’, ’lower’, ’nop’, ’raise’]

Exercise 4.1. What would the code for this machine look like if it were written without
using the generateOutput method?

4.2 Basic combination and abstraction of state machines


In the previous section, we studied the definition of a primitive state machine, and saw a number
of examples. State machines are useful for a wide variety of problems, but specifying complex
machines by explicitly writing out their state transition functions can be quite tedious. Ultimately,
we will want to build large state-machine descriptions compositionally, by specifying primitive
machines and then combining them into more complex systems. We will start here by looking at
ways of combining state machines.
We can apply our PCAP (primitive, combination, abstraction, pattern) methodology, to build
more complex SMs out of simpler ones. In the rest of this section we consider “dataflow” com-
positions, where inputs and outputs of primitive machines are connected together; after that, we
consider “conditional” compositions that make use of different sub-machines depending on the
input to the machine, and finally “sequential” compositions that run one machine after another.

4.2.1 Cascade composition


In cascade composition, we take two machines and use the output of the first one as the input
to the second, as shown in figure 4.4. The result is a new composite machine, whose input vo-
cabulary is the input vocabulary of the first machine and whose output vocabulary is the output
vocabulary of the second machine. It is, of course, crucial that the output vocabulary of the first
machine be the same as the input vocabulary of the second machine.

i1 m1 o1 = i2 m2 o2

Cascade(m1, m2)

Figure 4.4 Cascade composition of state machines

Recalling the Delay machine from the previous section, let’s see what happens if we make the
cascade composition of two delay machines. Let m1 be a delay machine with initial value 99
and m2 be a delay machine with initial value 22. Then Cascade(m1 , m2 ) is a new state machine,
constructed by making the output of m1 be the input of m2 . Now, imagine we feed a sequence of
values, 3, 8, 2, 4, 6, 5, into the composite machine, m. What will come out? Let’s try to understand
this by making a table of the states and values at different times:
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 125

time 0 1 2 3 4 5 6

m1 input 3 8 2 4 6 5
m1 state 99 3 8 2 4 6 5
m1 output 99 3 8 2 4 6
m2 input 99 3 8 2 4 6
m2 state 22 99 3 8 2 4 6
m2 output 22 99 3 8 2 4

The output sequence is 22, 99, 3, 8, 2, 4, which is the input sequence, delayed by two time steps.
Another way to think about cascade composition is as follows. Let the input to m1 at time t be
called i1 [t] and the output of m1 at time t be called o1 [t]. Then, we can describe the workings of
the delay machine in terms of an equation:

o1 [t] = i1 [t − 1] for all values of t > 0;


o1 [0] = init1

that is, that the output value at every time t is equal to the input value at the previous time step.
You can see that in the table above. The same relation holds for the input and output of m2 :

o2 [t] = i2 [t − 1] for all values of t > 0.


o2 [0] = init2

Now, since we have connected the output of m1 to the input of m2 , we also have that i2 [t] = o1 [t]
for all values of t. This lets us make the following derivation:

o2 [t] = i2 [t − 1]
= o1 [t − 1]
= i1 [t − 2]

This makes it clear that we have built a “delay by two” machine, by cascading two single delay
machines.
As with all of our systems of combination, we will be able to form the cascade composition not
only of two primitive machines, but of any two machines that we can make, through any set of
compositions of primitive machines.
Here is the Python code for an Increment machine. It is a pure function whose output at time t
is just the input at time t plus the constant incr. The safeAdd function is the same as addition, if
the inputs are numbers. We will see, later, why it is important.

class Increment(SM):
def __init__(self, incr):
self.incr = incr
def getNextState(self, state, inp):
return safeAdd(inp, self.incr)

Exercise 4.2. Derive what happens when you cascade two delay-by-two machines?
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 126

Exercise 4.3. What is the difference between these two machines?

>>> foo1 = sm.Cascade(sm.Delay(100), Increment(1))


>>> foo2 = sm.Cascade(Increment(1), sm.Delay(100))

Demonstrate by drawing a table of their inputs, states, and outputs, over


time.

4.2.2 Parallel composition


In parallel composition, we take two machines and run them “side by side”. They both take the
same input, and the output of the composite machine is the pair of outputs of the individual
machines. The result is a new composite machine, whose input vocabulary is the same as the
input vocabulary of the component machines (which is the same for both machines) and whose
output vocabulary is pairs of elements, the first from the output vocabulary of the first machine
and the second from the output vocabulary of the second machine. Figure 4.5 shows two types
of parallel composition; in this section we are talking about the first type.

i m1 o1 i1 m1 o1

m2 o2 i2 m2 o2

Parallel(m1, m2) Parallel2(m1, m2)

Figure 4.5 Parallel and Parallel2 compositions of state machines.

In Python, we can define a new class of state machines, called Parallel, which is a subclass of
SM. To make an instance of Parallel, we pass two SMs of any type into the initializer. The state of
the parallel machine is a pair consisting of the states of the constituent machines. So, the starting
state is the pair of the starting states of the constituents.

class Parallel (SM):


def __init__(self, sm1, sm2):
self.m1 = sm1
self.m2 = sm2
self.startState = (sm1.startState, sm2.startState)

To get a new state of the composite machine, we just have to get new states for each of the con-
stituents, and return the pair of them; similarly for the outputs.

def getNextValues(self, state, inp):


(s1, s2) = state
(newS1, o1) = self.m1.getNextValues(s1, inp)
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 127

(newS2, o2) = self.m2.getNextValues(s2, inp)


return ((newS1, newS2), (o1, o2))

Parallel2
Sometimes we will want a variant on parallel combination, in which rather than having the input
be a single item which is fed to both machines, the input is a pair of items, the first of which is
fed to the first machine and the second to the second machine. This composition is shown in the
second part of figure 4.5.
Here is a Python class that implements this two-input parallel composition. It can inherit the
__init__ method from ParallelSM, so use ParallelSM as the superclass, and we only have to
define two methods.

class Parallel2 (Parallel):


def getNextValues(self, state, inp):
(s1, s2) = state
(i1, i2) = splitValue(inp)
(newS1, o1) = self.m1.getNextValues(s1, i1)
(newS2, o2) = self.m2.getNextValues(s2, i2)
return ((newS1, newS2), (o1, o2))

The only complexity here arises because we have to handle the situation in which the input is
’undefined’. In that case, we want to pass ’undefined’ into the constituent machines. We
make our code more beautiful by defining the helper function below, which is guaranteed to
return a pair, if its argument is either a pair or ’undefined’. 33 We will see the reason that we
sometimes pass in a value of ’undefined’ in section 4.2.3.

def splitValue(v):
if v == ’undefined’:
return (’undefined’, ’undefined’)
else:
return v

ParallelAdd
The ParallelAdd state machine combination is just like Parallel, except that it has a single
output whose value is the sum of the outputs of the constituent machines. It is straightforward to
define:

class ParallelAdd (Parallel):


def getNextValues(self, state, inp):
(s1, s2) = state
(newS1, o1) = self.m1.getNextValues(s1, inp)
(newS2, o2) = self.m2.getNextValues(s2, inp)
return ((newS1, newS2), o1 + o2)

4.2.3 Feedback composition

33
We are trying to make the code examples we show here as simple and clear as possible; if we were writing code for
actual deployment, we would check and generate error messages for all sorts of potential problems (in this case, for
instance, if v is neither None nor a two-element list or tuple.)
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 128

i o
m o m

Feedback(m) Feedback2(m)

Figure 4.6 Two forms of feedback composition.

Another important means of combination that we will use frequently is the feedback combinator,
in which the output of a machine is fed back to be the input of the same machine at the next step,
as shown in figure 4.6. The first value that is fed back is the output associated with the initial state
of the machine on which we are operating. It is crucial that the input and output vocabularies of
the machine are the same (because the output at step t will be the input at step t + 1). Because
we have fed the output back to the input, this machine does not consume any inputs; but we will
treat the feedback value as an output of this machine.
Here is an example of using feedback to make a machine that counts. We can start with a simple
machine, an incrementer, that takes a number as input and returns that same number plus 1 as
the output. By itself, it has no memory. Here is its formal description:

S = numbers
I = numbers
O = numbers
n(s, i) = i + 1
o(s, i) = n(s, i)
s0 = 0

What would happen if we performed the feedback operation on this machine? We can try to
understand this in terms of the input/output equations. From the definition of the increment
machine, we have

o[t] = i[t] + 1 .

And if we connect the input to the output, then we will have

i[t] = o[t] .

And so, we have a problem; these equations cannot be satisfied.


A crucial requirement for applying feedback to a machine is: that machine must not have a direct
dependence of its output on its input.
We have already explored a Delay machine, which delays its output by one step. We can delay
the result of our incrementer, by cascading it with a Delay machine, as shown in figure 4.7. Now,
we have the following equations describing the system:
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 129

Incr Delay(0)

Counter

Figure 4.7 Counter made with feedback and serial combination of an incrementer and a delay.

oi [t] = ii [t] + 1
od [t] = id [t − 1]
ii [t] = od [t]
id [t] = oi [t]

The first two equations describe the operations of the increment and delay boxes; the second two
describe the wiring between the modules. Now we can see that, in general,

oi [t] = ii [t] + 1
oi [t] = od [t] + 1
oi [t] = id [t − 1] + 1
oi [t] = oi [t − 1] + 1

that is, that the output of the incrementer is going to be one greater on each time step.

Exercise 4.4. How could you use feedback and a negation primitive machine (which
is a pure function that takes a Boolean as input and returns the negation
of that Boolean) to make a machine whose output alternates between true
and false.

4.2.3.1 Python Implementation


Following is a Python implementation of the feedback combinator, as a new subclass of SM that
takes, at initialization time, a state machine.

class Feedback (SM):


def __init__(self, sm):
self.m = sm
self.startState = self.m.startState

The starting state of the feedback machine is just the state of the constituent machine.
Generating an output for the feedback machine is interesting: by our hypothesis that the output
of the constituent machine cannot depend directly on the current input, it means that, for the
purposes of generating the output, we can actually feed an explicitly undefined value into the
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 130

machine as input. Why would we do this? The answer is that we do not know what the input
value should be (in fact, it is defined to be the output that we are trying to compute).
We must, at this point, add an extra condition on our getNextValues methods. They have to
be prepared to accept ’undefined’ as an input. If they get an undefined input, they should
return ’undefined’ as an output. For convenience, in our files, we have defined the procedures
safeAdd and safeMul to do addition and multiplication, but passing through ’undefined’ if it
occurs in either argument.
So: if we pass ’undefined’ into the constituent machine’s getNextValues method, we must
not get ’undefined’ back as output; if we do, it means that there is an immediate dependence
of the output on the input. Now we know the output o of the machine.
To get the next state of the machine, we get the next state of the constituent machine, by taking
the feedback value, o, that we just computed and using it as input for getNextValues. This
will generate the next state of the feedback machine. (Note that throughout this process inp is
ignored—a feedback machine has no input.)

def getNextValues(self, state, inp):


(ignore, o) = self.m.getNextValues(state, ’undefined’)
(newS, ignore) = self.m.getNextValues(state, o)
return (newS, o)

Now, we can construct the counter we designed. The Increment machine, as we saw in its
definition, uses a safeAdd procedure, which has the following property: if either argument is
’undefined’, then the answer is ’undefined’; otherwise, it is the sum of the inputs.

def makeCounter(init, step):


return sm.Feedback(sm.Cascade(Increment(step), sm.Delay(init)))

>>> c = makeCounter(3, 2)
>>> c.run(verbose = True)
Start state: (None, 3)
Step: 0
Feedback_96
Cascade_97
Increment_98 In: 3 Out: 5 Next State: 5
Delay_99 In: 5 Out: 3 Next State: 5
Step: 1
Feedback_96
Cascade_97
Increment_98 In: 5 Out: 7 Next State: 7
Delay_99 In: 7 Out: 5 Next State: 7
Step: 2
Feedback_96
Cascade_97
Increment_98 In: 7 Out: 9 Next State: 9
Delay_99 In: 9 Out: 7 Next State: 9
Step: 3
Feedback_96
Cascade_97
Increment_98 In: 9 Out: 11 Next State: 11
Delay_99 In: 11 Out: 9 Next State: 11
Step: 4
Feedback_96
Cascade_97
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 131

Increment_98 In: 11 Out: 13 Next State: 13


Delay_99 In: 13 Out: 11 Next State: 13
...

[3, 5, 7, 9, 11, 13, 15, 17, 19, 21]

(The numbers, like 96 in Feedback_96 are not important; they are just tags generated internally
to indicate different instances of a class.)

Exercise 4.5. Draw state tables illustrating whether the following machines are differ-
ent, and if so, how:

m1 = sm.Feedback(sm.Cascade(sm.Delay(1),Increment(1)))
m2 = sm.Feedback(sm.Cascade(Increment(1), sm.Delay(1)))

4.2.3.2 Fibonacci
Now, we can get very fancy. We can generate the Fibonacci sequence (1, 1, 2, 3, 5, 8, 13, 21, etc),
in which the first two outputs are 1, and each subsequent output is the sum of the two previous
outputs, using a combination of very simple machines. Basically, we have to arrange for the
output of the machine to be fed back into a parallel combination of elements, one of which delays
the value by one step, and one of which delays by two steps. Then, those values are added, to
compute the next output. Figure 4.8 shows a diagram of one way to construct this system.

Delay(1)

+
Delay(1) Delay(0)

Fibonacci

Figure 4.8 Machine to generate the Fibonacci sequence.

The corresponding Python code is shown below. First, we have to define a new component ma-
chine. An Adder takes pairs of numbers (appearing simultaneously) as input, and immediately
generates their sum as output.

class Adder(SM):
def getNextState(self, state, inp):
(i1, i2) = splitValue(inp)
return safeAdd(i1, i2)

Now, we can define our fib machine. It is a great example of building a complex machine out of
very nearly trivial components. In fact, we will see in the next module that there is an interesting
and important class of machines that can be constructed with cascade and parallel compositions
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 132

of delay, adder, and gain machines. It is crucial for the delay machines to have the right values
(as shown in the figure) in order for the sequence to start off correctly.

>>> fib = sm.Feedback(sm.Cascade(sm.Parallel(sm.Delay(1),


sm.Cascade(sm.Delay(1), sm.Delay(0))),
Adder()))

>>> fib.run(verbose = True)


Start state: ((1, (1, 0)), None)
Step: 0
Feedback_100
Cascade_101
Parallel_102
Delay_103 In: 1 Out: 1 Next State: 1
Cascade_104
Delay_105 In: 1 Out: 1 Next State: 1
Delay_106 In: 1 Out: 0 Next State: 1
Adder_107 In: (1, 0) Out: 1 Next State: 1
Step: 1
Feedback_100
Cascade_101
Parallel_102
Delay_103 In: 2 Out: 1 Next State: 2
Cascade_104
Delay_105 In: 2 Out: 1 Next State: 2
Delay_106 In: 1 Out: 1 Next State: 1
Adder_107 In: (1, 1) Out: 2 Next State: 2
Step: 2
Feedback_100
Cascade_101
Parallel_102
Delay_103 In: 3 Out: 2 Next State: 3
Cascade_104
Delay_105 In: 3 Out: 2 Next State: 3
Delay_106 In: 2 Out: 1 Next State: 2
Adder_107 In: (2, 1) Out: 3 Next State: 3
Step: 3
Feedback_100
Cascade_101
Parallel_102
Delay_103 In: 5 Out: 3 Next State: 5
Cascade_104
Delay_105 In: 5 Out: 3 Next State: 5
Delay_106 In: 3 Out: 2 Next State: 3
Adder_107 In: (3, 2) Out: 5 Next State: 5
...

[1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

Exercise 4.6. What would we have to do to this machine to get the sequence [1, 1, 2,
3, 5, ...]?
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 133

Exercise 4.7. Define fib as a composition involving only two delay components and an
adder. You might want to use an instance of the Wire class.
A Wire is the completely passive machine, whose output is always in-
stantaneously equal to its input. It is not very interesting by itself, but
sometimes handy when building things.

class Wire(SM):
def getNextState(self, state, inp):
return inp

Exercise 4.8. Use feedback and a multiplier (analogous to Adder) to make a machine
whose output doubles on every step.

Exercise 4.9. Use feedback and a multiplier (analogous to Adder) to make a machine
whose output squares on every step.

4.2.3.3 Feedback2
The second part of figure 4.6 shows a combination we call feedback2 : it assumes that it takes a
machine with two inputs and one output, and connects the output of the machine to the second
input, resulting in a machine with one input and one output.
Feedback2 is very similar to the basic feedback combinator, but it gives, as input to the constituent
machine, the pair of the input to the machine and the feedback value.

class Feedback2 (Feedback):


def getNextValues(self, state, inp):
(ignore, o) = self.m.getNextValues(state, (inp, ’undefined’))
(newS, ignore) = self.m.getNextValues(state, (inp, o))
return (newS, o)

4.2.3.4 FeedbackSubtract and FeedbackAdd


In feedback addition composition, we take two machines and connect them as shown below:

+ m1

m2

If m1 and m2 are state machines, then you can create their feedback addition composition with

newM = sm.FeedbackAdd(m1, m2)

Now newM is itself a state machine. So, for example,


Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 134

newM = sm.FeedbackAdd(sm.R(0), sm.Wire())

makes a machine whose output is the sum of all the inputs it has ever had (remember that sm.R is
shorthand for sm.Delay). You can test it by feeding it a sequence of inputs; in the example below,
it is the numbers 0 through 9:

>>> newM.transduce(range(10))
[0, 0, 1, 3, 6, 10, 15, 21, 28, 36]

Feedback subtraction composition is the same, except the output of m2 is subtracted from the
input, to get the input to m1.

+ m1

m2

Note that if you want to apply one of the feedback operators in a situation where there is only one
machine, you can use the sm.Gain(1.0) machine (defined section 4.1.2.2.1), which is essentially
a wire, as the other argument.

4.2.3.5 Factorial
We will do one more tricky example, and illustrate the use of Feedback2. What if we
wanted to generate the sequence of numbers {1!, 2!, 3!, 4!, . . .} (where k! = 1 · 2 · 3 . . . · k)?
We can do so by multiplying the previous value of the sequence by a number equal to the
“index” of the sequence. Figure 4.9 shows the structure of a machine for solving this problem. It
uses a counter (which is, as we saw before, made with feedback around a delay and increment)
as the input to a machine that takes a single input, and multiplies it by the output value of the
machine, fed back through a delay.

Incr Delay(1)
* Delay(1)

Counter
Factorial

Figure 4.9 Machine to generate the Factorial sequence.

Here is how to do it in Python; we take advantage of having defined counter machines to abstract
away from them and use that definition here without thinking about its internal structure. The
initial values in the delays get the series started off in the right place. What would happen if we
started at 0?

fact = sm.Cascade(makeCounter(1, 1),


sm.Feedback2(sm.Cascade(Multiplier(), sm.Delay(1))))
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 135

>>> fact.run(verbose = True)


Start state: ((None, 1), (None, 1))
Step: 0
Cascade_1
Feedback_2
Cascade_3
Increment_4 In: 1 Out: 2 Next State: 2
Delay_5 In: 2 Out: 1 Next State: 2
Feedback2_6
Cascade_7
Multiplier_8 In: (1, 1) Out: 1 Next State: 1
Delay_9 In: 1 Out: 1 Next State: 1
Step: 1
Cascade_1
Feedback_2
Cascade_3
Increment_4 In: 2 Out: 3 Next State: 3
Delay_5 In: 3 Out: 2 Next State: 3
Feedback2_6
Cascade_7
Multiplier_8 In: (2, 1) Out: 2 Next State: 2
Delay_9 In: 2 Out: 1 Next State: 2
Step: 2
Cascade_1
Feedback_2
Cascade_3
Increment_4 In: 3 Out: 4 Next State: 4
Delay_5 In: 4 Out: 3 Next State: 4
Feedback2_6
Cascade_7
Multiplier_8 In: (3, 2) Out: 6 Next State: 6
Delay_9 In: 6 Out: 2 Next State: 6
Step: 3
Cascade_1
Feedback_2
Cascade_3
Increment_4 In: 4 Out: 5 Next State: 5
Delay_5 In: 5 Out: 4 Next State: 5
Feedback2_6
Cascade_7
Multiplier_8 In: (4, 6) Out: 24 Next State: 24
Delay_9 In: 24 Out: 6 Next State: 24
...

[1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880]

It might bother you that we get a 1 as the zeroth element of the sequence, but it is reasonable
as a definition of 0!, because 1 is the multiplicative identity (and is often defined that way by
mathematicians).

4.2.4 Plants and controllers


One common situation in which we combine machines is to simulate the effects of coupling a
controller and a so-called “plant”. A plant is a factory or other external environment that we
might wish to control. In this case, we connect two state machines so that the output of the plant
(typically thought of as sensory observations) is input to the controller, and the output of the
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 136

controller (typically thought of as actions) is input to the plant. This is shown schematically in
figure 4.10. For example, when you build a Soar brain that interacts with the robot, the robot (and
the world in which it is operating) is the “plant” and the brain is the controller. We can build a
coupled machine by first connecting the machines in a cascade and then using feedback on that
combination.

Plant

Controller

Figure 4.10 Two coupled machines.

As a concrete example, let’s think about a robot driving straight toward a wall. It has a distance
sensor that allows it to observe the distance to the wall at time t, d[t], and it desires to stop at
some distance ddesired . The robot can execute velocity commands, and we program it to use the
following rule to set its velocity at time t, based on its most recent sensor reading:

v[t] = K(ddesired − d[t − 1]) .

This controller can also be described as a state machine, whose input sequence is the observed
values of d and whose output sequence is the values of v.

S = numbers
I = numbers
O = numbers
n(s, i) = K(ddesired − i)
o(s) = s
s0 = dinit

Now, we can think about the “plant”; that is, the relationship between the robot and the world.
The distance of the robot to the wall changes at each time step depending on the robot’s forward
velocity and the length of the time steps. Let δT be the length of time between velocity commands
issued by the robot. Then we can describe the world with the equation:

d[t] = d[t − 1] − δT v[t − 1] .

which assumes that a positive velocity moves the robot toward the wall (and therefore decreases
the distance). This system can be described as a state machine, whose input sequence is the values
of the robot’s velocity, v, and whose output sequence is the values of its distance to the wall, d.
Finally, we can couple these two systems, as for a simulator, to get a single state machine with no
inputs. We can observe the sequence of internal values of d and v to understand how the system
is behaving.
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 137

In Python, we start by defining the controller machine; the values k and dDesired are constants
of the whole system.

k = -1.5
dDesired = 1.0
class WallController(SM):
def getNextState(self, state, inp):
return safeMul(k, safeAdd(dDesired, safeMul(-1, inp)))

The output being generated is actually k * (dDesired - inp), but because this method is go-
ing to be used in a feedback machine, it might have to deal with ’undefined’ as an input. It has
no delay built into it.
Think about why we want k to be negative. What happens when the robot is closer to the wall
than desired? What happens when it is farther from the wall than desired?
Now, we can define a class that describes the behavior of the “plant”:

deltaT = 0.1
class WallWorld(SM):
startState = 5
def getNextValues(self, state, inp):
return (state - deltaT * inp, state)

Setting startState = 5 means that the robot starts 5 meters from the wall. Note that the output
of this machine does not depend instantaneously on the input; so there is a delay in it.
Now, we can defined a general combinator for coupling two machines, as in a plant and controller:

def coupledMachine(m1, m2):


return sm.Feedback(sm.Cascade(m1, m2))

We can use it to connect our controller to the world, and run it:

>>> wallSim = coupledMachine(WallController(), WallWorld())


>>> wallSim.run(30)
[5, 4.4000000000000004, 3.8900000000000001, 3.4565000000000001,
3.088025, 2.77482125, 2.5085980624999999, 2.2823083531249999,
2.0899621001562498, 1.9264677851328122, 1.7874976173628905,
1.6693729747584569, 1.5689670285446884, 1.483621974262985,
1.4110786781235374, 1.3494168764050067, 1.2970043449442556,
1.2524536932026173, 1.2145856392222247, 1.1823977933388909,
1.1550381243380574, 1.1317824056873489, 1.1120150448342465,
1.0952127881091096, 1.0809308698927431, 1.0687912394088317,
1.058472553497507, 1.049701670472881, 1.0422464199019488,
1.0359094569166565]

Because WallWorld is the second machine in the cascade, its output is the output of the whole
machine; so, we can see that the distance from the robot to the wall is converging monotonically
to dDesired (which is 1).

Exercise 4.10. What kind of behavior do you get with different values of k?
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 138

4.2.5 Conditionals
We might want to use different machines depending on something that is happening in the out-
side world. Here we describe three different conditional combinators, that make choices, at the
run-time of the machine, about what to do.

4.2.6 Switch
We will start by considering a conditional combinator that runs two machines in parallel, but
decides on every input whether to send the input into one machine or the other. So, only one of
the parallel machines has its state updated on each step. We will call this switch, to emphasize the
fact that the decision about which machine to execute is being re-made on every step.
Implementing this requires us to maintain the states of both machines, just as we did for parallel
combination. The getNextValues method tests the condition and then gets a new state and
output from the appropriate constituent machine; it also has to be sure to pass through the old
state for the constituent machine that was not updated this time.

class Switch (SM):


def __init__(self, condition, sm1, sm2):
self.m1 = sm1
self.m2 = sm2
self.condition = condition
self.startState = (self.m1.startState, self.m2.startState)

def getNextValues(self, state, inp):


(s1, s2) = state
if self.condition(inp):
(ns1, o) = self.m1.getNextValues(s1, inp)
return ((ns1, s2), o)
else:
(ns2, o) = self.m2.getNextValues(s2, inp)
return ((s1, ns2), o)

Multiplex
The switch combinator takes care to only update one of the component machines; in some other
cases, we want to update both machines on every step and simply use the condition to select the
output of one machine or the other to be the current output of the combined machine.
This is a very small variation on Switch, so we will just implement it as a subclass.

class Mux (Switch):


def getNextValues(self, state, inp):
(s1, s2) = state
(ns1, o1) = self.m1.getNextValues(s1, inp)
(ns2, o2) = self.m2.getNextValues(s2, inp)
if self.condition(inp):
return ((ns1, ns2), o1)
else:
return ((ns1, ns2), o2)
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 139

Exercise 4.11. What is the result of running these two machines

m1 = Switch(lambda inp: inp > 100,


Accumulator(),
Accumulator())
m2 = Mux(lambda inp: inp > 100,
Accumulator(),
Accumulator())

on the input

[2, 3, 4, 200, 300, 400, 1, 2, 3]

Explain why they are the same or are different.

If
Feel free to skip this example; it is only useful in fairly complicated contexts.
The If combinator. It takes a condition, which is a function from the input to true or false,
and two machines. It evaluates the condition on the first input it receives. If the value
is true then it executes the first machine forever more; if it is false, then it executes the second
machine.
This can be straightforwardly implemented in Python; we will work through a slightly simplified
version of our code below. We start by defining an initializer that remembers the conditions and
the two constituent state machines.

class If (SM):
startState = (’start’, None)
def __init__(self, condition, sm1, sm2):
self.sm1 = sm1
self.sm2 = sm2
self.condition = condition

Because this machine does not have an input available at start time, it can not decide whether it
is going to execute sm1 or sm2. Ultimately, the state of the If machine will be a pair of values:
the first will indicate which constituent machine we are running and the second will be the state
of that machine. But, to start, we will be in the state (’start’, None), which indicates that the
decision about which machine to execute has not yet been made.
Now, when it is time to do a state update, we have an input. We destructure the state into its
two parts, and check to see if the first component is ’start’. If so, we have to make the decision
about which machine to execute. The method getFirstRealState first calls the condition on
the current input, to decide which machine to run; then it returns the pair of a symbol indicating
which machine has been selected and the starting state of that machine. Once the first real state is
determined, then that is used to compute a transition into an appropriate next state, based on the
input.
If the machine was already in a non-start state, then it just updates the constituent state, using the
already-selected constituent machine. Similarly, to generate an output, we have use the output
function of the appropriate machine, with special handling of the start state.
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 140

startState = (’start’, None)

def __init__(self, condition, sm1, sm2):


self.sm1 = sm1
self.sm2 = sm2
self.condition = condition

def getFirstRealState(self, inp):


if self.condition(inp):
return (’runningM1’, self.sm1.startState)
else:
return (’runningM2’, self.sm2.startState)

def getNextValues(self, state, inp):


(ifState, smState) = state
if ifState == ’start’:
(ifState, smState) = self.getFirstRealState(inp)

if ifState == ’runningM1’:
(newS, o) = self.sm1.getNextValues(smState, inp)
return ((’runningM1’, newS), o)
else:
(newS, o) = self.sm2.getNextValues(smState, inp)
return ((’runningM2’, newS), o)

4.3 Terminating state machines and sequential compositions


So far, all the machines we have discussed run forever; or, at least, until we quit giving them
inputs. But in some cases, it is particularly useful to think of a process as consisting of a sequence
of processes, one executing until termination, and then another one starting. For example, you
might want to robot to clean first room A, and then clean room B; or, for it to search in an area
until it finds a person and then sound an alarm.
Temporal combinations of machines form a new, different PCAP system for state machines. Our
primitives will be state machines, as described above, but with one additional property: they will
have a termination or done function, d(s), which takes a state and returns true if the machine has
finished execution and false otherwise.
Rather than defining a whole new class of state machines (though we could do that), we will
just augment the SM class with a default method, which says that, by default, machines do not
terminate.

def done(self, state):


return False

Then, in the definition of any subclass of SM, you are free to implement your own done method
that will override this base one. The done method is used by state machine combinators that, for
example, run one machine until it is done, and then switch to running another one.
Here is an example terminating state machine (TSM) that consumes a stream of numbers; its
output is None on the first four steps and then on the fifth step, it generates the sum of the numbers
it has seen as inputs, and then terminates. It looks just like the state machines we have seen before,
with the addition of a done method. Its state consists of two numbers: the first is the number of
times the machine has been updated and the second is the total input it has accumulated so far.
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 141

class ConsumeFiveValues(SM):
startState = (0, 0) # count, total

def getNextValues(self, state, inp):


(count, total) = state
if count == 4:
return ((count + 1, total + inp), total + inp)
else:
return ((count + 1, total + inp), None)

def done(self, state):


(count, total) = state
return count == 5

Here is the result of running a simple example. We have modified the transduce method of SM
to stop when the machine is done.

>>> c5 = ConsumeFiveValues()
>>> c5.transduce([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], verbose = True)
Start state: (0, 0)
In: 1 Out: None Next State: (1, 1)
In: 2 Out: None Next State: (2, 3)
In: 3 Out: None Next State: (3, 6)
In: 4 Out: None Next State: (4, 10)
In: 5 Out: 15 Next State: (5, 15)
[None, None, None, None, 15]

Now we can define a new set of combinators that operate on TSMs. Each of these combina-
tors assumes that its constituent machines are terminating state machines, and are, themselves,
terminating state machines. We have to respect certain rules about TSMs when we do this. In
particular, it is not legal to call the getNextValues method on a TSM that says it is done. This
may or may not cause an actual Python error, but it is never a sensible thing to do, and may result
in meaningless answers.

4.3.1 Repeat
The simplest of the TSM combinators is one that takes a terminating state machine sm and repeats
it n times. In the Python method below, we give a default value of None for n, so that if no value
is passed in for n it will repeat forever.

class Repeat (SM):


def __init__(self, sm, n = None):
self.sm = sm
self.startState = (0, self.sm.startState)
self.n = n

The state of this machine will be the number of times the constituent machine has been executed
to completion, together with the current state of the constituent machine. So, the starting state is
a pair consisting of 0 and the starting state of the constituent machine.
Because we are going to, later, ask the constituent machine to generate an output, we are going
to adopt a convention that the constituent machine is never left in a state that is done, unless
the whole Repeat is itself done. If the constituent machine is done, then we will increment the
counter for the number of times we have repeated it, and restart it. Just in case the constituent
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 142

machine “wakes up” in a state that is done, we use a while loop here, instead of an if: we will
keep restarting this machine until the count runs out. Why? Because we promised not to leave
our constituent machine in a done state (so, for example, nobody asks for its output, when its
done), unless the whole repeat machine is done as well.

def advanceIfDone(self, counter, smState):


while self.sm.done(smState) and not self.done((counter, smState)):
counter = counter + 1
smState = self.sm.startState
return (counter, smState)

To get the next state, we start by getting the next state of the constituent machine; then, we check
to see if the counter needs to be advanced and the machine restarted; the advanceIfDone method
handles this situation and returns the appropriate next state. The output of the Repeat machine
is just the output of the constituent machine. We just have to be sure to destructure the state of
the overall machine and pass the right part of it into the constituent.

def getNextValues(self, state, inp):


(counter, smState) = state
(smState, o) = self.sm.getNextValues(smState, inp)
(counter, smState) = self.advanceIfDone(counter, smState)
return ((counter, smState), o)

We know the whole Repeat is done if the counter is equal to n.

def done(self, state):


(counter, smState) = state
return counter == self.n

Now, we can see some examples of Repeat. As a primitive, here is a silly little example TSM. It
takes a character at initialization time. Its state is a Boolean, indicating whether it is done. It starts
up in state False (not done). Then it makes its first transition into state True and stays there. Its
output is always the character it was initialized with; it completely ignores its input.

class CharTSM (SM):


startState = False
def __init__(self, c):
self.c = c
def getNextValues(self, state, inp):
return (True, self.c)
def done(self, state):
return state

>>> a = CharTSM(’a’)
>>> a.run(verbose = True)
Start state: False
In: None Out: a Next State: True
[’a’]

See that it terminates after one output. But, now, we can repeat it several times.

>>> a4 = sm.Repeat(a, 4)
>>> a4.run()
[’a’, ’a’, ’a’, ’a’]
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 143

Exercise 4.12. Would it have made a difference if we had executed:

>>> sm.Repeat(CharTSM(’a’), 4).run()

Exercise 4.13. Monty P. thinks that the following call

>>> sm.Repeat(ConsumeFiveValues(), 3).transduce(range(100))

will generate a sequence of 14 Nones followed by the sum of the first 15


integers (starting at 0). R. Reticulatis disagrees. Who is right and why?

4.3.2 Sequence
Another useful thing to do with TSMs is to execute several different machines sequentially. That
is, take a list of TSMs, run the first one until it is done, start the next one and run it until it is done,
and so on. This machine is similar in style and structure to a Repeat TSM. Its state is a pair of
values: an index that says which of the constituent machines is currently being executed, and the
state of the current constituent.
Here is a Python class for creating a Sequence TSM. It takes as input a list of state machines; it
remembers the machines and number of machines in the list.

class Sequence (SM):


def __init__(self, smList):
self.smList = smList
self.startState = (0, self.smList[0].startState)
self.n = len(smList)

The initial state of this machine is the value 0 (because we start by executing the 0th constituent
machine on the list) and the initial state of that constituent machine.
The method for advancing is also similar that for Repeat. The only difference is that each time,
we start the next machine in the list of machines, until we have finished executing the last one.

def advanceIfDone(self, counter, smState):


while self.smList[counter].done(smState) and counter + 1 < self.n:
counter = counter + 1
smState = self.smList[counter].startState
return (counter, smState)

To get the next state, we ask the current constituent machine for its next state, and then, if it is
done, advance the state to the next machine in the list that is not done when it wakes up. The
output of the composite machine is just the output of the current constituent.

def getNextValues(self, state, inp):


(counter, smState) = state
(smState, o) = self.smList[counter].getNextValues(smState, inp)
(counter, smState) = self.advanceIfDone(counter, smState)
return ((counter, smState), o)
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 144

We have constructed this machine so that it always advances past any constituent machine that is
done; if, in fact, the current constituent machine is done, then the whole machine is also done.

def done(self, state):


(counter, smState) = state
return self.smList[counter].done(smState)

We can make good use of the CharTSM to test our sequential combinator. First, we will try some-
thing simple:

>>> m = sm.Sequence([CharTSM(’a’), CharTSM(’b’), CharTSM(’c’)])


>>> m.run()
Start state: (0, False)
In: None Out: a Next State: (1, False)
In: None Out: b Next State: (2, False)
In: None Out: c Next State: (2, True)
[’a’, ’b’, ’c’]

Even in a test case, there is something unsatisfying about all that repetitive typing required to
make each individual CharTSM. If we are repeating, we should abstract. So, we can write a func-
tion that takes a string as input, and returns a sequential TSM that will output that string. It uses
a list comprehension to turn each character into a CharTSM that generates that character, and then
uses that sequence to make a Sequence.

def makeTextSequenceTSM(str):
return sm.Sequence([CharTSM(c) for c in str])

>>> m = makeTextSequenceTSM(’Hello World’)


>>> m.run(20, verbose = True)
Start state: (0, False)
In: None Out: H Next State: (1, False)
In: None Out: e Next State: (2, False)
In: None Out: l Next State: (3, False)
In: None Out: l Next State: (4, False)
In: None Out: o Next State: (5, False)
In: None Out: Next State: (6, False)
In: None Out: W Next State: (7, False)
In: None Out: o Next State: (8, False)
In: None Out: r Next State: (9, False)
In: None Out: l Next State: (10, False)
In: None Out: d Next State: (10, True)
[’H’, ’e’, ’l’, ’l’, ’o’, ’ ’, ’W’, ’o’, ’r’, ’l’, ’d’]

We can also see that sequencing interacts well with the Repeat combinator.

>>> m = sm.Repeat(makeTextSequenceTSM(’abc’), 3)
>>> m.run(verbose = True)
Start state: (0, (0, False))
In: None Out: a Next State: (0, (1, False))
In: None Out: b Next State: (0, (2, False))
In: None Out: c Next State: (1, (0, False))
In: None Out: a Next State: (1, (1, False))
In: None Out: b Next State: (1, (2, False))
In: None Out: c Next State: (2, (0, False))
In: None Out: a Next State: (2, (1, False))
In: None Out: b Next State: (2, (2, False))
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 145

In: None Out: c Next State: (3, (0, False))


[’a’, ’b’, ’c’, ’a’, ’b’, ’c’, ’a’, ’b’, ’c’]

It is interesting to understand the state here. The first value is the number of times the constituent
machine of the Repeat machine has finished executing; the second value is the index of the se-
quential machine into its list of machines, and the last Boolean is the state of the CharTSM that is
being executed, which is an indicator for whether it is done or not.

4.3.3 RepeatUntil and Until


In order to use Repeat, we need to know in advance how many times we want to execute the
constituent TSM. Just as in ordinary programming, we often want to terminate when a particular
condition is met in the world. For this purpose, we can construct a new TSM combinator, called
RepeatUntil. It takes, at initialization time, a condition, which is a function from an input to
a Boolean, and a TSM. It runs the TSM to completion, then tests the condition on the input; if
the condition is true, then the RepeatUntil terminates; if it is false, then it runs the TSM to
completion again, tests the condition, etc.
Here is the Python code for implementing RepeatUntil. The state of this machine has two parts:
a Boolean indicating whether the condition is true, and the state of the constituent machine.

class RepeatUntil (SM):


def __init__(self, condition, sm):
self.sm = sm
self.condition = condition
self.startState = (False, self.sm.startState)

def getNextValues(self, state, inp):


(condTrue, smState) = state
(smState, o) = self.sm.getNextValues(smState, inp)
condTrue = self.condition(inp)
if self.sm.done(smState) and not condTrue:
smState = self.sm.getStartState()
return ((condTrue, smState), o)

def done(self, state):


(condTrue, smState) = state
return self.sm.done(smState) and condTrue

One important thing to note is that, in the RepeatUntil TSM the condition is only evaluated
when the constituent TSM is done. This is appropriate in some situations; but in other cases, we
would like to terminate the execution of a TSM if a condition becomes true at any single step of
the machine. We could easily implement something like this in any particular case, by defining
a special-purpose TSM class that has a done method that tests the termination condition. But,
because this structure is generally useful, we can define a general-purpose combinator, called
Until. This combinator also takes a condition and a constituent machine. It simply executes
the constituent machine, and terminates either when the condition becomes true, or when the
constituent machine terminates. As before, the state includes the value of the condition on the
last input and the state of the constituent machine.
Note that this machine will never execute the constituent machine more than once; it will either
run it once to completion (if the condition never becomes true), or terminate it early.
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 146

Here are some examples of using RepeatUntil and Until. First, we run the ConsumeFive-
Values machine until the input is greater than 10. Because it only tests the condition when
ConsumeFiveValues is done, and the condition only becomes true on the 11th step, the Con-
sumeFiveValues machine is run to completion three times.

def greaterThan10 (x):


return x > 10
>>> m = sm.RepeatUntil(greaterThan10, ConsumeFiveValues())
>>> m.transduce(range(20), verbose = True)
Start state: (0, 0)
In: 0 Out: None Next State: (1, 0)
In: 1 Out: None Next State: (2, 1)
In: 2 Out: None Next State: (3, 3)
In: 3 Out: None Next State: (4, 6)
In: 4 Out: 10 Next State: (0, 0)
In: 5 Out: None Next State: (1, 5)
In: 6 Out: None Next State: (2, 11)
In: 7 Out: None Next State: (3, 18)
In: 8 Out: None Next State: (4, 26)
In: 9 Out: 35 Next State: (0, 0)
In: 10 Out: None Next State: (1, 10)
In: 11 Out: None Next State: (2, 21)
In: 12 Out: None Next State: (3, 33)
In: 13 Out: None Next State: (4, 46)
In: 14 Out: 60 Next State: (5, 60)
[None, None, None, None, 10, None, None, None, None, 35, None, None, None, None, 60]

If we do Until on the basic ConsumeFiveValues machine, then it just runs ConsumeFiveValues


until it terminates normally, because the condition never becomes true during this time.

>>> m = sm.Until(greaterThan10, ConsumeFiveValues())


>>> m.transduce(range(20), verbose = True)
Start state: (False, (0, 0))
In: 0 Out: None Next State: (False, (1, 0))
In: 1 Out: None Next State: (False, (2, 1))
In: 2 Out: None Next State: (False, (3, 3))
In: 3 Out: None Next State: (False, (4, 6))
In: 4 Out: 10 Next State: (False, (5, 10))
[None, None, None, None, 10]

However, if we change the termination condition, the execution will be terminated early. Note
that we can use a lambda expression directly as an argument; sometimes this is actually clearer
than defining a function with def, but it is fine to do it either way.

>>> m = sm.Until(lambda x: x == 2, ConsumeFiveValues())


>>> m.transduce(range(20), verbose = True)
Start state: (False, (0, 0))
In: 0 Out: None Next State: (False, (1, 0))
In: 1 Out: None Next State: (False, (2, 1))
In: 2 Out: None Next State: (True, (3, 3))
[None, None, None]

If we actually want to keep repeating ConsumeFiveValues() until the condition becomes true,
we can combine Until with Repeat. Now, we see that it executes the constituent machine mul-
tiple times, but terminates as soon as the condition is satisfied.
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 147

>>> m = sm.Until(greaterThan10, sm.Repeat(ConsumeFiveValues()))


>>> m.transduce(range(20), verbose = True)
Start state: (False, (0, (0, 0)))
In: 0 Out: None Next State: (False, (0, (1, 0)))
In: 1 Out: None Next State: (False, (0, (2, 1)))
In: 2 Out: None Next State: (False, (0, (3, 3)))
In: 3 Out: None Next State: (False, (0, (4, 6)))
In: 4 Out: 10 Next State: (False, (1, (0, 0)))
In: 5 Out: None Next State: (False, (1, (1, 5)))
In: 6 Out: None Next State: (False, (1, (2, 11)))
In: 7 Out: None Next State: (False, (1, (3, 18)))
In: 8 Out: None Next State: (False, (1, (4, 26)))
In: 9 Out: 35 Next State: (False, (2, (0, 0)))
In: 10 Out: None Next State: (False, (2, (1, 10)))
In: 11 Out: None Next State: (True, (2, (2, 21)))
[None, None, None, None, 10, None, None, None, None, 35, None, None]

4.4 Using a state machine to control the robot


This section gives an overview of how to control the robot with a state machine. For a much more
detailed description, see the Infrastructure Guide, which documents the io and util modules
in detail. The io module provides procedures and methods for interacting with the robot; the
util module provides procedures and methods for doing computations that are generally useful
(manipulating angles, dealing with coordinate frames, etc.)
We can implement a robot controller as a state machine whose inputs are instances of class
io.SensorInput, and whose outputs are instances of class io.Action.
Here is Python code for a brain that is controlled by the most basic of state machines. This machine
always emits the default action, io.Action(), which sets all of the output values to zero. When
the brain is set up, we create a “behavior”, which is a name we will use for a state machine that
transduces a stream of io.SensorInputs to a stream of io.Actions. Finally, we ask the behavior
to start.
Then, all we do in the step method of the robot is:
• Read the sensors, by calling io.SensorInput() to get an instance that contains sonar and
odometry readings;
• Feed that sensor input to the brain state machine, by calling its step method with that as input;
and
• Take the io.action that is generated by the brain as output, and call its execute method,
which causes it to actually send motor commands to the robot.

You can set the verbose flag to True if you want to see a lot of output on each step for debugging.
Inside a Soar brain, we have access to an object robot, which persists during the entire execution
of the brain, and gives us a place to store important objects (like the state machine that will be
doing all the work).

import sm
import io

class StopSM(sm.SM):
def getNextValues(self, state, inp):
return (None, io.Action())
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 148

def setup():
robot.behavior = StopSM()
robot.behavior.start()

def step():
robot.behavior.step(io.SensorInput(), verbose = False).execute()

In the following sections we will develop two simple machines for controlling a robot to move a
fixed distance or turn through a fixed angle. Then we will put them together and explore why it
can be useful to have the starting state of a machine depend on the input.

4.4.1 Rotate
Imagine that we want the robot to rotate a fixed angle, say 90 degrees, to the left of where it
is when it starts to run a behavior. We can use the robot’s odometry to measure approximately
where it is, in an arbitrary coordinate frame; but to know how much it has moved since we started,
we have to store some information in the state.
Here is a class that defines a Rotate state machine. It takes, at initialization time, a desired change
in heading.

class RotateTSM (SM):


rotationalGain = 3.0
angleEpsilon = 0.01
startState = ’start’

def __init__(self, headingDelta):


self.headingDelta = headingDelta

When it is time to start this machine, we would like to look at the robot’s current heading (theta),
add the desired change in heading, and store the result in our state as the desired heading. Then,
in order to test whether the behavior is done, we want to see whether the current heading is close
enough to the desired heading. Because the done method does not have access to the input of the
machine (it is a property only of states), we need to include the current theta in the state. So, the
state of the machine is (thetaDesired, thetaLast).
Thus, the getNextValues method looks at the state; if it is the special symbol ’start’, it means
that the machine has not previously had a chance to observe the input and see what its current
heading is, so it computes the desired heading (by adding the desired change to the current head-
ing, and then calling a utility procedure to be sure the resulting angle is between plus and minus
π), and returns it and the current heading. Otherwise, we keep the thetaDesired component
of the state, and just get a new value of theta out of the input. We generate an action with a
rotational velocity that will rotate toward the desired heading with velocity proportional to the
magnitude of the angular error.

def getNextValues(self, state, inp):


currentTheta = inp.odometry.theta
if state == ’start’:
thetaDesired = \
util.fixAnglePlusMinusPi(currentTheta + self.headingDelta)
else:
(thetaDesired, thetaLast) = state
newState = (thetaDesired, currentTheta)
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 149

action = io.Action(rvel = self.rotationalGain * \


util.fixAnglePlusMinusPi(thetaDesired - currentTheta))

return (newState, action)

Finally, we have to say which states are done. Clearly, the ’start’ state is not done; but we are
done if the most recent theta from the odometry is within some tolerance, self.angleEpsilon,
of the desired heading.

def done(self, state):


if state == ’start’:
return False
else:
(thetaDesired, thetaLast) = state
return util.nearAngle(thetaDesired, thetaLast, self.angleEpsilon)

Exercise 4.14. Change this machine so that it rotates through an angle, so you could give
it 2 pi or minus 2 pi to have it rotate all the way around.

4.4.2 Forward
Moving the robot forward a fixed distance is similar. In this case, we remember the robot’s x
and y coordinates when it starts, and drive straight forward until the distance between the initial
position and the current position is close to the desired distance. The state of the machine is the
robot’s starting position and its current position.

class ForwardTSM (SM):


forwardGain = 1.0
distTargetEpsilon = 0.01
startState = ’start’

def __init__(self, delta):


self.deltaDesired = delta

def getNextValues(self, state, inp):


currentPos = inp.odometry.point()
if state == ’start’:
print "Starting forward", self.deltaDesired
startPos = currentPos
else:
(startPos, lastPos) = state
newState = (startPos, currentPos)
error = self.deltaDesired - startPos.distance(currentPos)
action = io.Action(fvel = self.forwardGain * error)
return (newState, action)

def done(self, state):


if state == ’start’:
return False
else:
(startPos, lastPos) = state
return util.within(startPos.distance(lastPos),
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 150

self.deltaDesired,
self.distTargetEpsilon)

4.4.3 Square Spiral


Imagine we would like to have the robot drive in a square spiral, similar to the one shown in
figure 4.11. One way to approach this problem is to make a “low-level” machine that can consume
a goal point and the sensor input and drive (in the absence of obstacles) to the goal point; and
then make a “high-level” machine that will keep track of where we are in the figure and feed goal
points to the low-level machine.

0.903

-0.756

-0.756 0.904

Figure 4.11 Square spiral path of the robot using the methods in this section.

4.4.3.1 XYDriver
Here is a class that describes a machine that takes as input a series of pairs of goal points (ex-
pressed in the robot’s odometry frame) and sensor input structures. It generates as output a
series of actions. This machine is very nearly a pure function machine, which has the following
basic control structure:
• If the robot is headed toward the goal point, move forward.
• If it is not headed toward the goal point, rotate toward the goal point.
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 151

This decision is made on every step, and results in a robust ability to drive toward a point in
two-dimensional space.
For many uses, this machine does not need any state. But the modularity is nicer, in some cases,
if it has a meaningful done method, which depends only on the state. So, we will let the state
of this machine be whether it is done or not. It needs several constants to govern rotational and
forward speeds, and tolerances for deciding whether it is pointed close enough toward the target
and whether it has arrived close enough to the target.

class XYDriver(SM):
forwardGain = 2.0
rotationGain = 2.0
angleEps = 0.05
distEps = 0.02
startState = False

The getNextValues method embodies the control structure described above.

def getNextValues(self, state, inp):


(goalPoint, sensors) = inp
robotPose = sensors.odometry
robotPoint = robotPose.point()
robotTheta = robotPose.theta

if goalPoint == None:
return (True, io.Action())

headingTheta = robotPoint.angleTo(goalPoint)
if util.nearAngle(robotTheta, headingTheta, self.angleEps):
# Pointing in the right direction, so move forward
r = robotPoint.distance(goalPoint)
if r < self.distEps:
# We’re there
return (True, io.Action())
else:
return (False, io.Action(fvel = r * self.forwardGain))
else:
# Rotate to point toward goal
headingError = util.fixAnglePlusMinusPi(\
headingTheta - robotTheta)
return (False, io.Action(rvel = headingError * self.rotationGain))

The state of the machine is just a boolean indicating whether we are done.

def done(self, state):


return state

4.4.3.2 Cascade approach


We make a spiral by building a machine that takes SensorInput objects as input and generates
pairs of subgoals and sensorinputs; such a machine can be cascaded with XYDriver to generate
a spiral.
Our implementation of this is a class called SpyroGyra. It takes the increment (amount that each
new side is larger than the previous) at initialization time. Its state consists of three components:
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 152

• direction: one of ’north’, ’south’, ’east’, or ’west’, indicating which way the robot is traveling
• length: length in meters of the current line segment being followed
• subGoal: the point in the robot’s odometry frame that defines the end of the current line
segment
It requires a tolerance to decide when the current subgoal point has been reached.

class SpyroGyra(SM):
distEps = 0.02
def __init__(self, incr):
self.incr = incr
self.startState = (’south’, 0, None)

If the robot is close enough to the subgoal point, then it is time to change the state. We increment
the side length, pick the next direction (counter clockwise around the cardinal compass direc-
tions), and compute the next subgoal point. The output is just the subgoal and the sensor input,
which is what the driver needs.

def getNextValues(self, state, inp):


(direction, length, subGoal) = state
robotPose = inp.odometry
robotPoint = robotPose.point()
if subGoal == None:
subGoal = robotPoint
if robotPoint.isNear(subGoal, self.distEps):
# Time to change state
length = length + self.incr
if direction == ’east’:
direction = ’north’
subGoal.y += length
elif direction == ’north’:
direction = ’west’
subGoal.x -= length
elif direction == ’west’:
direction = ’south’
subGoal.y -= length
else: # south
direction = ’east’
subGoal.x += length
print ’new:’, direction, length, subGoal
return ((direction, length, subGoal),
(subGoal, inp))

Finally, to make the spiral, we just cascade these two machines together.

def spiroFlow(incr):
return sm.Cascade(SpyroGyra(incr), XYDriver())

Exercise 4.15. What explains the rounded sides of the path in figure 4.11?
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 153

4.5 Conclusion

State machines
State machines are such a general formalism, that a huge class of discrete-time systems can be
described as state machines. The system of defining primitive machines and combinations gives
us one discipline for describing complex systems. It will turn out that there are some systems
that are conveniently defined using this discipline, but that for other kinds of systems, other
disciplines would be more natural. As you encounter complex engineering problems, your job
is to find the PCAP system that is appropriate for them, and if one does not exist already, invent
one.
State machines are such a general class of systems that although it is a useful framework for
implementing systems, we cannot generally analyze the behavior of state machines. That is, we
can’t make much in the way of generic predictions about their future behavior, except by running
them to see what will happen.
In the next module, we will look at a restricted class of state machines, whose state is representable
as a bounded history of their previous states and previous inputs, and whose output is a linear
function of those states and inputs. This is a much smaller class of systems than all state machines,
but it is nonetheless very powerful. The important lesson will be that restricting the form of the
models we are using will allow us to make stronger claims about their behavior.

Knuth on Elevator Controllers


Donald E. Knuth is a computer scientist who is famous for, among other things, his series of
textbooks (as well as for TEX, the typesetting system we use to make all of our handouts), and a
variety of other contributions to theoretical computer science.
“It is perhaps significant to note that although the author had used the elevator system for years
and thought he knew it well, it wasn’t until he attempted to write this section that he realized
there were quite a few facts about the elevator’s system of choosing directions that he did not
know. He went back to experiment with the elevator six separate times, each time believing he
had finally achieved a complete understanding of its modus operandi. (Now he is reluctant to
ride it for fear some new facet of its operation will appear, contradicting the algorithms given.)
We often fail to realize how little we know about a thing until we attempt to simulate it on a
computer.”
The Art of Computer Programming, Donald E., Knuth, Vol 1. page 295. On the elevator system
in the Mathematics Building at Cal Tech. First published in 1968

4.6 Examples

4.6.1 Practice problem: Things


Consider the following program

def thing(inputList):
output = []
i = 0
for x in range(3):
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 154

y = 0
while y < 100 and i < len(inputList):
y = y + inputList[i]
output.append(y)
i = i + 1
return output

A. What is the value of

thing([1, 2, 3, 100, 4, 9, 500, 51, -2, 57, 103, 1, 1, 1, 1, -10, 207, 3, 1])

[1, 3, 6, 106, 4, 13, 513, 51, 49, 106]

It’s important to understand the loop structure of the Python program: It goes through (at
most) three times, and adds up the elements of the input list, generating a partial sum as
output on each step, and terminating the inner loop when the sum becomes greater than 100.

B. Write a single state machine class MySM such that MySM().transduce(inputList) gives the
same result as thing(inputList), if inputList is a list of numbers. Remember to include a
done method, that will cause it to terminate at the same time as thing.
class MySM(sm.SM):
startState = (0,0)
def getNextValues(self, state, inp):
(x, y) = state
y += inp
if y >= 100:
return ((x + 1, 0), y)
return ((x, y), y)
def done(self, state):
(x, y) = state
return x >= 3

The most important step, conceptually, is deciding what the state of the machine will be.
Looking at the original Python program, we can see that we had to keep track of how many
times we had completed the outer loop, and then what the current partial sum was of the
inner loop.
The getNextValues method first increments the partial sum by the input value, and then
checks to see whether it’s time to reset. If so, it increments the ’loop counter’ (x) component
of the state and resets the partial sum to 0. It’s important to remember that the output of the
getNextValues method is a pair, containing the next state and the output.
The done method just checks to see whether we have finished three whole iterations.

C. Recall the definition of sm.Repeat(m, n): Given a terminating state machine m, it returns
a new terminating state machine that will execute the machine m to completion n times, and
then terminate.
Use sm.Repeat and a very simple state machine that you define to create a new state machine
MyNewSM, such that MyNewSM is equivalent to an instance of MySM.
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 155

class Sum(sm.SM):
startState = 0
def getNextValues(self, state, inp):
return (state + inp, state + inp)
def done(self, state):
return state > 100

myNewSM = sm.Repeat(Sum(), 3)

4.6.2 Practice problem: Inheritance and State Machines


Recall that we have defined a Python class sm.SM to represent state machines. Here we consider
a special type of state machine, whose states are always integers that start at 0 and increment by
1 on each transition. We can represent this new type of state machines as a Python subclass of
sm.SM called CountingStateMachine.
We wish to use the CountingStateMachine class to define new subclasses that each provide a
single new method getOutput(self, state, inp) which returns just the output for that state
and input; the CountingStateMachine will take care of managing and incrementing the state,
so its subclasses don’t have to worry about it.
Here is an example of a subclass of CountingStateMachine.

class CountMod5(CountingStateMachine):
def getOutput(self, state, inp):
return state % 5

Instances of CountMod5 generate output sequences of the form 0, 1, 2, 3, 4, 0, 1, 2, 3,


4, 0, . . ..
Part a. Define the CountingStateMachine class. Since CountingStateMachine is a subclass
of sm.SM, you will have to provide definitions of the startState instance variable and get-
NextValues method, just as we have done for other state machines. You can assume that every
subclass of CountingStateMachine will provide an appropriate getOutput method.

class CountingStateMachine(sm.SM):
def __init__(self):
self.startState = 0
def getNextState(self, state, inp):
return(state + 1, self.getOutput(state, inp))

Part b. Define a subclass of CountingStateMachine called AlternateZeros. Instances of


AlternateZeros should be state machines for which, on even steps, the output is the same as
the input, and on odd steps, the output is 0. That is, given inputs, i0 , i1 , i2 , i3 , . . ., they generate
outputs, i0 , 0, i2 , 0, . . ..
Chapter 4 State Machines 6.01— Spring 2011— February 1, 2011 156

class AlternateZeros(CountingStateMachine):
def getOutput(self, state, inp):
if not state % 2:
return inp
return 0

You might also like