TeachScheme teaches principles. What do the rest of us teach?
Bottom line: I challenge anyone who takes an objects-first approach to teaching introductory programming to provide an account of the order in which concepts are introduced tied to the CS principles they illustrate and to articulate reasons for their ordering based on cognitive load for the students (a paper reference will do, too).
I recently spent a deeply engaging and thought-provoking week learning about the TeachScheme curriculum with Kathi Fisler from Worcester Polytechnic Institute and Shriram Krishnamurthi from Brown. Both have worked with the curriculum since its inception at Rice around 15 years ago. During that decade and a half, they have contributed to incremental refinements and seen it taught at hundreds of high schools and colleges. My initial thoughts on the workshop can be read here; I’ve now been reflecting on the material for a few weeks and offer some higher level thoughts.
What’s your course’s underlying philosophy?
The authors of the TeachScheme curriculum note that “college is the only time in a programmer’s life when he is exposed to principled ideas on a regular and rigorous basis” . Unlike most approaches to teaching introductory programming, which can be labeled as ad-hoc at best, TeachScheme is based on several ideas woven through all aspects of the highly-structured curriculum:
- Use of design recipes as a way to systematize the programming process and provide students with a starting point
- Testing as an integral part of programming
- Introduction of syntax only as needed to illustrate broader principles
- Use of an environment which provides error messages appropriate for a learner’s level
While these underlying ideas may seem fairly simple and language-agnostic, I think it quickly becomes obvious that they are not frequently followed in CS1 courses and more subtly, that many modern languages actually guide instructors away from them.
Take the focus on testing, for example. A Java instructor might say “Aha, I’ll just introduce JUnit early.” Unfortunately, understanding JUnit requires a lot of background knowledge on things like inheritance, static methods, reflection… sure, one could probably use it just fine by pattern matching, but I think that process takes away from the simple goal: figure out whether an algorithm does what it’s supposed to. For a beginner, the challenge in testing should be figuring out appropriate test cases, not expressing them. Worse even is that much of Java code, whether it’s object-oriented or not, includes changing variables and various kinds of output. These make verifying whether a certain piece of code works as intended potentially quite difficult.
What about introducing syntax in a measured, systematic way? That’s something I strongly value because I think students with lower computing self-esteem (girls and ethnic minorities) are particularly hard-hit by syntactic ambiguity. A lot of my hot-headed boys will play around until they figure out how a particular construct works and not be phased if it doesn’t match their initial expectations but I’ve seen many girls think they were dumb simply because I hadn’t properly explained all the possible errors they might encounter in using a new syntactic element.
Most advocates of languages other than Java pounce on “public-static-void-main-string-bracket-bracket-args” as a prohibitively convoluted way of starting a program. In practice, though it’s irritating and I hate having to introduce it without being able to give much explanation initially, I don’t find that particularly alarming for students — I have them learn it as an incantation and/or copy it and eventually they do understand all parts. That said, I think the incantation is symptomatic of a broader problem of dependency: lots of constructs in imperative languages are interconnected. For example, to have any hope of using an iterative construct (while or for loop), a student is going to need to understand the syntax for variable assignment and mutation (storing a value in a variable and changing it). Even worse, to reliably use variables, a student will need to understand things about memory. So many levels of knowledge and understanding are required that we can’t get to solving problems — we’re stuck with syntax and semantics. Python, PHP, C and your favorite obscure imperative language all include these challenges.
Racket: a Scheme variant
As I mentioned in my last post on the workshop, the ideas which have guided the creation and refinement of the TeachScheme curriculum really resonate with me. In fact, there’s a lot of overlap with the principles that guide my own instruction but I’m all too aware that I constantly make significant compromises. For example, I don’t have students use formal testing methodology but I have them diff their output (both text and graphical) to expected output. That’s certainly not as systematic or scalable as I would like. Another example of a compromise is the previously-mentioned main method header — I accept that I’ll explain it throughout the semester but I still grind my teeth a little at night because of its initial lack of clarity. My practice is peppered with these uneasy compromises primarily dictated by the fact that I teach procedural languages.
In contrast, the originators of TeachScheme have been unwilling to compromise on the underlying principles. They are able to do this by using Racket, a variant of Scheme. The language has been modified to reflect the needs of the curriculum. For example, it has a check-expect function which can be used inline with other Racket code to compare the result of two expressions:
This makes testing functions simple and systematic. I think the fact that the language and environment support this style of testing allows some great early conversation on a fundamental principle of computer science: algorithm correctness.
I’ve also attempted to use this trivial example to illustrate the design process emphasized by How to Design Programs, the free textbook that uses the TeachScheme philosophy. Most approaches to teaching programming rely on showing lots of examples for students to learn from. TeachScheme is based on the notion that ” implicit learning does not work well for the majority of students. Most students are not the type of learner who can extract abstract principles on program design from a series of examples” . Instead, we should explicitly describe the process of going from a problem statement to a program. In the example above, I’ve got a comment at the top that includes the types of inputs and outputs (the contract) as well as a short purpose statement. Writing those comments and the tests were part of the design process. Of course, this becomes much more meaningful when operating on more complex data such as lists of CDs:
I think the first thing to notice is that a complex data type is defined concisely using a struct at the very top. There’s no need for a constructor or default values as would be the case if writing a corresponding class. This enables the discussion of representing and manipulating fairly complex data without getting bogged down in syntax and concepts like mutation.
The three comments following the struct definition certainly look a little odd but they have their reason. The first block is a “template” for writing a function that processes a CD. This is an example of a technique used to reduce the “blank page anxiety.” If a student writes this template, they can see that the only things that can possibly be done by a function that operates on a CD are manipulating its title, its count or its category.
The second comment block defines what a list of CDs is. Again, kind of weird for those not used to functional programming, but it has its elegance. There’s no need to define a new data type for a list of CDs, hence the comment status. The cons function is used to build linked lists “on the fly.” The comment is again used to reduce “blank page anxiety” and guide students who are getting lost.
The third comment block defines a template for what a function that consumes a list of CDs should look like. Unsurprisingly, my total-stock function looks awfully similar to it.
I also wrote some sample data as well as a check-expect BEFORE writing the total-stock function. With the framework in place, writing the function is very simple and verifying its validity is even simpler. This may seem like a lot of work for a three-line function but it pays off as examples get more and more complex. For example, take a look at a filesystem assignment. I attempted it in an ad hoc manner and got bogged down in all the mutually recursive data definitions. With the templates, I was done in no time.
Why isn’t it more popular?
Principles may be front and center, but TeachScheme is not a fashionable approach. First of all, the functional programming paradigm is not very common. A quick glance at the charts on langpop.com shows imperative and OO languages like Java, C, C++, PHP, etc at the top and functional languages like Lisp, Scheme, Haskell, etc, pretty low on the ladder. That can be discouraging for students who hope to immediately be able to get an internship or write something that looks like a Windows native application.
Secondly, the signal to noise ratio in functional programming is very high. In other words, programs are generally quite short, but every line requires deliberate thought. I think that makes a lot of people shy away from it and treat it as harder than imperative programming. Related to that, there’s a kind of inertia: if instructors learned procedural or OO languages first, it’s only natural that we’d be uncomfortable reaching out of our expertise, especially to something we have considered harder. I think the sacrifice here is that although functional programming leverages existing math knowledge, algebra is already quite hard for a lot of people, thus not easily available for transfer. I also think most people consider software in terms of what it does rather than what it computes and that lends itself better to imperative programming — do this, then change that, then call that routine, etc.
Unfortunately, I suspect the real reason TeachScheme isn’t more prominent has more to do with egos than with technical merit.
Scorn is not a good advertising tactic
Before going to this excellent workshop, I had heard of TeachScheme but generally not in a very positive light. One of the other workshop participants summed it up in a way I really liked — “scorn is not a good advertising tactic.” First of all, most of the functional programmers I’ve known, from high school and beyond, have been incredibly brilliant and unwilling to suffer fools, otherwise known as people-who-program-in-PHP-omg-that’s-a-disgusting-language — people like me. As a result, I was, as many people are, mostly intimidated by functional languages.
Then, there is an aura of religious zealotry and inflexibility around the TeachScheme materials. I think a lot of people feel threatened by the vague “if you don’t think this is the right way to teach intro CS you are dumb” vibes exuded by the project. I don’t know quite where those come from, but I don’t think I’m the only one who has felt them.
My next steps
I’m going to be using the TeachScheme curriculum in the fall with my advanced AP class composed of students with some prior programming experience. I think they’re a good group to focus on principles with because they have enough background to grasp the implications of these principles.
I’m a little more willing to compromise, though, and I’m not ready to make a complete switch. That said, I think I can be more deliberate about using a lot of the principled ideas in my procedural to OO classes in Java and Python. It’s also become even more clear to me that starting with objects first is not of interest to me — I think the sacrifices are too great.
Above all, I’m going to continue my search for a single other coherent approach to teaching introductory programming which has gone through such methodical review. I’m disappointed not to have found any; pointers would be appreciated.