the-structure-and-interpretation-of-the-computer-science-curriculum
the-structure-and-interpretation-of-the-computer-science-curriculum
c 2004 Cambridge University Press 365
DOI: 10.1017/S0956796804005076 Printed in the United Kingdom
EDUCATIONAL PEARL
The structure and interpretation of the
computer science curriculum
MATTHIAS FELLEISEN
Northeastern University, Boston, MA, USA
MATTHEW FLATT
University of Utah, Salt Lake City, UT, USA
SHRIRAM KRISHNAMURTHI
Brown University, Providence, RI, USA
Abstract
Twenty years ago Abelson and Sussman’s Structure and Interpretation of Computer Programs
radically changed the intellectual landscape of introductory computing courses. Instead
of teaching some currently fashionable programming language, it employed Scheme and
functional programming to teach important ideas. Introductory courses based on the book
showed up around the world and made Scheme and functional programming popular.
Unfortunately, these courses quickly disappeared again due to shortcomings of the book
and the whimsies of Scheme. Worse, the experiment left people with a bad impression of
Scheme and functional programming in general. In this pearl, we propose an alternative role
for functional programming in the first-year curriculum. Specifically, we present a framework
for discussing the first-year curriculum and, based on it, the design rationale for our book
and course, dubbed How to Design Programs. The approach emphasizes the systematic design
of programs. Experience shows that it works extremely well as a preparation for a course on
object-oriented programming.
In short, sicp, Scheme, and functional programming don’t prepare students properly
for other programming courses and thus fail to meet a basic need.
Advocates of Scheme and functional programming alike must be concerned about
these reactions. To address them and to overcome the problems of the sicp approach,
we present this pearl. It consists of three pieces: a structural framework for analyzing
the first-year computing curriculum; an interpretation of sicp with respect to this
framework;2 and our alternative to the sicp approach that overcomes sicp’s problems
while retaining the essence of Scheme and functional programming.
2 Structure
2.1 Solving constraints
The primary goal of a computing curriculum is to produce programmers and
software engineers. After all, most of its graduates accept industry positions and
produce software. Many will stay involved with software production for a long
1 According to Bob Prior (editor at MIT Press), sicp sold 45,000 copies in its first five years (Personal
communication, 9 June 2003).
2 We chose sicp as our yardstick because it is the most widely used and known text that uses functional
programming and because we believe that all other texts – of almost equal age (Bird & Wadler, 1988)
or of recent vintage (Hudak, 2000) – on functional programming suffer from similar flaws.
time, even if only as managers, and therefore also need to learn to adapt to the
ever-evolving nature of the field.
Translating the primary goal into a set of goals for the introductory curriculum
is a difficult task because various groups impose a range of unrelated constraints.
Faculty colleagues (inside and outside of computer science) often have an emotional
preference for a specific language in the introductory course. To some, the first
language is the one that they know and work(ed) with. To others, it is the currently
fashionable industry language, e.g. C++ and Java over the past ten years.
Some computer science faculty demand that the first course teach languages that
are used in upstream courses. Sometimes they believe that the instructor of the
second course should not have to start from scratch and that the simplest solution
is to use a single programming language. Sometimes they wish to expose students
to languages that are used in popular upstream courses such as operating systems.
First-year students also come with strong, preconceived notions about program-
ming and computing. Some students (or their parents) have read about the latest
industry trends in popular magazines, such as (in the US) Time, Newsweek and
US News and World Report, and expect to see some of these things in a freshman
course. Some base their understanding on prior experiences in high schools. The
latter group is used to sophisticated development environments (IDEs) that include
mechanical support for syntactic conventions, GUI development, etc.
The state of the first-year students’ education adds another set of constraints
to the mix. Some understand calculus; for others, even rudimentary algebra is a
minefield.
Finally, students also have a wide range of expectations. Some students wish
to learn what computer science is about; others have three years of programming
experience. Some wish to know why things work; others want to learn how to
construct games. Almost everyone expects that their college training will help them
find internships and professional positions.
Satisfying the primary goal of producing software professionals subject to these
constraints poses a complex problem. On one hand, learning to program well requires
a lot of practice and in particular a lot of hands-on practice. Hence, early courses
must introduce programming and must choose a specific programming language.
On the other hand, choosing one language over another must disappoint some
constituents, and we must therefore convey to them our choices with good reasons.
After all, education is as much about satisfying human needs as it is about technical
correctness.
We propose to solve this constraint problem with a second look at the primary
goal and the timing constraints. Clearly, a computer science curriculum must not,
and doesn’t have to, become a vocational training ground for the latest industrial
programming language and programming tools. Superficial aspects of industrial
practice change as fast as fashion trends. No academic department can switch
its course content fast enough and maintain a curriculum that passes on tested
wisdom. Still, when students cross over from academia into industry, they must be
prepared to program and ideally to program well. From this perspective, two points
in the curriculum take on special meaning: the first summer, when students work
in internship positions, and the last year, when students interview for their first
full-time positions.
Following this reasoning, we believe it is natural to concentrate on principles for
most of the time and to accommodate industrial needs during the second semester of
the first year and the last year of a college program. Considering that college is the
only time in a programmer’s life when he is exposed to principled ideas on a regular
and rigorous basis, the idea of emphasizing principles in college is obvious. Once a
programmer has a full-time position, there are too many constraints and distraction
for principled additional education. At the same time, however, a curriculum must
also teach how these principles apply to the real world. Nobody can expect students
to take this step on their own. In short, teach good habits early; otherwise bad
habits become ingrained and require costly fixes – just like bugs in programs.
Applied to the first-year courses, these suggestions say that the year should start
with a heavy emphasis on principles and should add some industrially relevant
components during the second semester. Even more precisely, the first semester
should emphasize programming principles and habits; the second part should
illustrate the use of these principles in currently fashionable programming languages.
Of course, the “principled” semester may integrate fashionable parts where they
aren’t an obstacle, and, more importantly, the “fashionable” part of the first year
must continue to practice good design habits.
3. Students must learn to use the examples developed in item (1c) above. They
must learn to calculate through examples before they code. They must learn to
translate the examples into automatic test suites, so that they can test programs
as they create them and as the programs evolve later.
More concisely, students must learn that programming requires far more than writing
down code and running it on some haphazardly chosen examples afterwards.
The last point in particular suggests that functional languages with their natural
model-view separation are superior choices for this first year. When students write
automatic test suites, they must to split a program into a part that deals with
computation proper (the “model”) and another part that interacts with the user (the
“view”). They then use the model in two distinct contexts: with a test suite and with
the view. To re-use the model in a test suite context, they don’t want to print results
but hand them over directly to a comparison function. Put differently, teaching
good software architecture principles to beginners requires function composition
and discourages a programming style that is primarily about reading and printing
values.
3 Challenging instructors throw in ideas from data structures and algorithms or, worse, pose problems
that require significant domain knowledge, that is, knowledge about non-computing topics. The problem
is then that students tend to confuse algorithms and application domain knowledge with program
construction, and neither helps students come up with good program organizations on their own when
they are left to their own devices.
require new constructs, but in the end it forces students to understand how to go
from data to design explicitly, and they will pick up language constructs implicitly.
Since most students are active learners, it is important to retain the example-
driven strategy that is currently used. The examples must, however, focus on the
use of program design principles in new situations instead of the use of language
constructs.
In summary, the first course should introduce the principles of program design,
state them explicitly as habits, and have students practice them with numerous
data-driven examples. To avoid any confusion, the course should not pose problems
from complex application domains and it should not use a complex language that
distracts from the design principles.
program design principles. They learn to think about values and operations on
values. They can easily comprehend how the functions and operations work with
values. Better still, they can use the same rules to figure out why a program produces
the wrong values, which it often will. Teaching an object-oriented language in the
second course is then a small shift of focus. It requires instructors to spend more
time on the syntactic complexities of the language, yet they can still rely on, and
reinforce, the design principles of the first course. In particular, the switch is of
a mostly syntactic nature, because the focus on designing classes of data and
operations on these classes remains the same.
4 In the 1970s, instructors who taught PL/1 faced a similar challenge and came up with a similar
solution, though without the full compiler support for error messages that we provide (Holt et al.,
1977).
5 It may still be valuable to teach some of these concepts later in the course, when students have
absorbed the basic ideas of program construction.
6 This partly explains why C++ is such a failure. Its lack of safety does not even guarantee that when
a program prints a number, it is actually interpreting bits that represent a number. Similarly, core
dumps and bus errors are much worse than exceptions, because they typically happen long after the
first violation occurred.
Scheme is dynamically typed. The lack of a type system means that we don’t have to
spend energy on finding and explaining type errors with the same care with which
we explain syntax errors. Better yet, when we use Scheme to teach design principles
we can informally superimpose a type system and use the types for program design.
In particular, it is easy to define and use sets and subsets of Scheme values. This
comes close to students’ intuitions about classes and subclasses in object-oriented
programs and thus provides a good transition for the second course.
sicp: htdp:
primality moving circles
interval arithmetic hangman
symbolic differentiation moving shapes
representing sets moving pictures
huffman encoding trees rearranging words
symbolic algebra binary search trees
digital circuits evaluating scheme
more on web pages
evaluating scheme again
moving pictures, again
mathematical examples
Gaussian elimination
normal/applicative order checking (on) queens
strictness/laziness accumulators on trees
non-determinism missionaries and cannibals
logic programming board solitaire
register machines exploring places
compilers moving pictures, a last time
it does so without explaining how to recognize situations in which one is more useful
than the other.
More generally, sicp doesn’t state how to program and how to manage the design
of a program. It leaves these things implicit and implies that students can discover a
discipline of design and programming on their own. The course presents the various
uses and roles of programming ideas with a series of examples. Some exercises then
ask students to modify this code basis, requiring students to read and study code;
others ask them to solve similar problems, which means they have to study the
construction and to change it to the best of their abilities. In short, sicp students
learn by copying and modifying code, which is barely an improvement over typical
programming text books.
sicp’s second major problem concerns its selection of examples and exercises. All
of these use complex domain knowledge. Consider the left column in figure 1. It
presents the choice of major examples that are used in the first few chapters of sicp.
Some early sections and the last two chapters cover topics from computer science:
see lower half of the left column in Figure 1.
While these topics are interesting to students who use computing in electrical
engineering and to those who already have significant experience of programming
and computing, they assume too much understanding from students who haven’t
understood programming yet and they assume too much domain knowledge from
any beginning student who needs to acquire program design skills. On the average,
beginners are not interested in mathematics and electrical engineering, and they do
not have ready access to the domain knowledge necessary for solving the domain
problems. As a result, sicp students must spend a considerable effort on the domain
knowledge and often end up confusing domain knowledge and program design
knowledge. They may even come to the conclusion that programming is a shallow
activity and that what truly matters is an understanding of domain knowledge.7
Similarly, many students lack an understanding of the role of compilers, logical
models of program execution, and so on. While first-semester students should
definitely find out about these ideas, they should do so in a context that reaffirms
the program design lessons.
In summary, while sicp does an excellent job shifting the focus of the first course
to challenging computer science topics, it fails to recognize the role of the first course
in the overall curriculum. In particular, sicp’s implicit approach to program design
ideas and its emphasis on complex domains obscures the goal of the first course as
seen from the perspective of a typical four-year curriculum.
7 Some faculty members argue that a course on introductory programming is a good place for teaching
students mathematical problem solving. While we partly agree with the idea that programming can
teach domain knowledge, we also believe that a course on programming should teach knowledge about
program design. We therefore ignore this line of argument here.
8 Glaser et al.’s notion of “programming by numbers” (Glaser et al., 2000) is a simple version of our
notion of a design recipe. It uses a version of step 4 in our design recipes for functions on algebraic
datatypes without going through the preparatory steps.
The book contains a series of approximately 10 design recipes. The first half of the
series shows how the description of the classes of data suggest a natural organization
of the functions that process them. These recipes addresses the design of functions
for classes of atomic data (numbers, booleans, characters), intervals and unions,
composites, self-referential definitions, groups of mutually referential definitions,
and so on. The second half of the series cover other important topics: abstracting
over similar functions and data definitions, generative recursion, accumulator-style
programming, and programming with mutation. In these cases, the design recipes
especially address the topic of when to use a technique or mode of an existing recipe;
no technique is introduced as just another trick for the toolbox.
The recipes also introduce a new distinction into program design: structural versus
generative recursion. The structural design recipes in the first half of the book match
the structure of a function to the structure of a data definition. When the data
definition happens to be self-referential, the function is recursive; when there is
a group of definitions with mutual cross-references, there is a group of function
definitions with mutual references among the functions. In contrast, generative
recursion concerns the generation of new problem data in the middle of the problem
solving process and the re-use of the problem solving method.
Compare insort and kwik , two standard sort functions:
The first function, insort, recurs on a structural portion of the given datum,
namely, (rest l ). The second function, kwik , recurs on data that are generated
by some other functions. To design a structurally recursive function is usually a
straightforward process. To design a generative recursive function, however, almost
always requires some ad hoc insight into the process. Often this insight is derived
from some mathematical idea. In addition, while structurally recursive functions
naturally terminate for all inputs, a generative recursive function may diverge. htdp
therefore suggests that students add a discussion about termination to the definition
of generative recursive functions.
Distinguishing the two forms of recursion and focusing on the structural case
makes our approach scalable to the object-oriented (OO) world. In an OO world,
the structural recipes naturally suggest class hierarchies and recursive methods that
call directly along containment (“has a”) relationships. Indeed, an OO purist might
five times as many A’s (best grade) as the C++ students; at the other grade levels,
the numbers are approximately the same.9
High school teachers who implement htdp report similar success stories as
colleges but in a less measurable manner. Still, the htdp curriculum has had an
interesting measurable effect concerning female students. Several instructors reported
that female students like the htdp curriculum exceptionally well. In a controlled
experiment, an htdp-trained instructor taught a conventional AP curriculum and
the Scheme curriculum to the same three classes of students. Together the three
classes consisted of over 70 students. While all students preferred our approach
to programming, the preference among females was a stunning factor of four. An
independent evaluator is now investigating this aspect of the project in more depth.
In general, we believe that the htdp project has validated the usefulness of
functional programming and functional programming languages in the first program-
ming course. We have found that teaching Scheme for Scheme’s sake (or Haskell
for Haskell’s sake) won’t work. Combining sicp with a GUI-based development
environment for Scheme won’t work better than plain sicp. The two keys to our
success were to tame Scheme into teaching languages that beginners can handle and
to distill well-known functional principles of programming into generally applicable
design recipes. Then we could show our colleagues that a combination of functional
programming as a preparation for a course on object-oriented programming is
an effective and indeed superior alternative to a year on just C++, Java, or a
combination.
We are hoping that other functional communities can replicate our success in
different contexts. We suggest, however, that using plain Erlang, Haskell, or ML and
that teaching programming in these languages implicitly will not do. We all need to
understand the role of functional programming in our curricula and the needs of our
students. Fortunately, Chakravarty & Keller’s (2004) recent educational pearl shows
that we are not the only ones who have recognized the deficiencies of conventional
approaches.
Note: DrScheme and How to Design Programs are freely available on the Web at
http://www.teach-scheme.org/.
References
Abelson, H., Sussman, G. J. and Sussman, J. (1985) Structure and interpretation of Computer
Programs. MIT Press.
Bird, R. and Wadler, P. (1988) Introduction to Functional Programming. Prentice Hall, New
York.
Chakravarty, M. M. T. and Keller, G. (2004) The risks and benefits of teaching purely
functional programming in first year. J. Functional Program. 14(1), 113–123.
Clements, J., Flatt, M. and Felleisen, M. (2001) Modeling an algebraic stepper. European
Symposium on Programming.
9 The numbers are normalized. The sample is approximately 150 students with approximately 40 students
from C++ and the rest from an htdp course.
Clinger, W. (1985) The revised revised report on the algorithmic language Scheme. Joint technical
report, Indiana University and MIT.
Clinger, W. and Rees, J. (1991) The revised4 report on the algorithmic language Scheme.
ACM Lisp Pointers, 4(3).
Findler, R. B., Clements, J., Flanagan, C., Flatt, M., Krishnamurthi, S., Steckler, P. and
Felleisen, M. (2002) DrScheme: A programming environment for Scheme. J. Functional
Program. 12(2), 159–182. (A preliminary version of this paper appeared in PLILP 1997,
LNCS 1292, pp. 369–388, Springer-Verlag.).
Glaser, H., Hartel, P. H. and Garratt, P. W. (2000) Programming by numbers – a programming
method for complete novices. Comput. J. 43(4), 252–265.
Holt, R. C., Wortman, D. B., Barnard, D. T. and Cordy, J. R. (1977) Sp/k: A system for
teaching computer programming. Comm. ACM 20(5), 301–309.
Hudak, P. (2000) The Haskell School of Expression. Cambridge University Press.
Jackson, D. and Chapin, J. (2000) Redesigning air traffic control: An exercise in software
design. IEEE Softw. 17(3).
Kelsey, R., Clinger, W. and Rees, J. (editors) (1998) Revised5 report of the algorithmic
language Scheme. ACM SIGPLAN Notices, 33(9), 26–76.
Raymond, E. S. (1998) The cathedral and the bazaar. First Monday, 3(3).
Schemer’s Inc. (1991) EdScheme: A modern Lisp.
Steele Jr., G. L. and Sussman, G. L. (1978) The revised report on scheme, a dialect of lisp.
Technical report 452, MIT Artificial Intelligence Laboratory.
Sussman, G. L. and Steele Jr., G. L. (1975) Scheme: An interpreter for extended lambda calculus.
Technical report 349, MIT Artificial Intelligence Laboratory.
van Orman Quine, W. (1963) Set Theory and its Logic. Harvard Press.
Wadler, P. (1987) A critique of Abelson and Sussman, or, why calculating is better than
scheming. SIGPLAN Notices, 22(3).