24 Oct 2021
Last week I was studying outside of a lecture hall where someone was teaching an introductory course on computer programming. There was a lot that I overheard that I disagreed with; this essay is an attempt to help me crystallize what exactly I disagreed with.
What is programming? What is good programming? What should programming be like? How you answer depends a lot on what you value. What I value in programming has not always been the same, and I think I’m the better for having toured around the space a little bit. I recognize that there’s still a lot to explore; nonetheless, I present my admittedly limited perspective on some broad ways that people think about programming—especially in academia and pedagogy—and some of the strengths and weaknesses of each.
One way of thinking about programming is that you are ordering a computer to do your bidding: you, the programmer, sit at the helm of your CPU, afloat on a sea of data, and you have various levers and knobs that you can pull and twist to make the CPU get from point A to point B: load this value into memory slot
i. Now add five to it. Now print that back out. Etc. This is called imperative programming, because you tell the computer every step it should take.
I’m doubtful that there’s much deep insight into programming this way.1 It’s like writing a recipe for a horrendously unimaginative cook. If you’re teaching students how to program like this, they might get an appreciation for being detail-oriented and sweating the minutiae—that’s well and good—but I think that’s about where it stops.
Another way of thinking about programming is that you, the programmer, teach the computer to solve progressively more complicated problems by composing bits of behavior together. Programs start behaving more like the Fourier series: the aggregation of simple, easy-to-understand components yields a robust and flexible result.
Where it really starts getting interesting is when you bring more powerful programming languages into the mix: languages that let you do more than just give the computer a dumb set of instructions to dutifully and meticulously slog through. Languages like Scheme (and related languages like Racket, Clojure, and—I’d argue—Elixir) give you the tools to build up models of the problem you’re trying to solve. You begin to think about the fundamental nature of the problems at hand and how to proceed from there. Whereas in the first case, you’re more focused on how to get the computer to do something. Reversing your tack can lead you down a wrong path for a long time without you knowing it.
Besides, high-level, functional programming languages make great pedagogical tools for more reasons than just the power they give you in modeling your program. Many of these languages put emphasis on building programs up from small, composable units with no side-effects that are easy to reason about, test, and put together. Not only do you learn how to sweat the details, you also learn how to orchestrate many simple pieces into complex solutions that fit the problem at hand. It’s the difference of being taught the rudiments of cooking and learning how to compose dishes that fit together into a complete meal meant to delight and nourish.
There’s some effort to achieve these ends when courses opt to focus on object-oriented principles. Sure, you learn about decomposing your problems along domain lines, but there’s often much more focus on mutation which can trip beginners up. Local-reasoning dissolves, and your layers of abstraction leak.
Furthermore, OO is emphatically not a good fit for so many problems! Nevertheless, a great deal of effort has been expended by thousands of researchers to find “best-practice patterns” for each and every scenario. We’ve drifted back a little towards the rote recipe-following instruction. Abstract mathematics provides a much richer modeling domain—indeed, computability theory was born from the Lambda Calculus, and it has proved to be a very fruitful field for general modeling.
Abstract mathematics isn’t a prerequisite to learning how to program (though it does turn out to be very useful the further one goes) but that doesn’t prevent us from teaching a more mathematically-oriented way of thinking about problem decomposition. There are many excellent books that do this, from the celebrated Structure and Interpretation of Computer Programs by Abelson and Sussman to How to Design Programs by Felleisen et. al.
My hope is that programming courses in higher-education settings (and high school settings!) will move away from imperative and even object-oriented programming towards a more functional approach.
Low-level knowledge #
I’d like to qualify an earlier claim: imperative programming has little benefit from a pedagogical standpoint.2 Now, that’s not entirely true, because there comes a point where you need to know the low-level details of how a computer works, and assembly is an imperative language. C is a great language for learning systems programming, because it exposes you to all the nuts and bolts of memory, interrupts, system calls, etc. There are a lot of footguns in this area, and that’s because physics is a beast. I still don’t think it’s a good idea to start with this stuff, much like a budding chef doesn’t need to know the details of the chemical reactions taking place in the oven, but there comes a point where knowing the underlying chemistry becomes indispensable.
Now, there’s a very wrong way to teach programming, and that way is by using something as ungainly as C++.
C++ is a pedagogically worthless language. It bogs a budding student down with historical baggage like header files and cryptic imports whilst drowning said student in the complexities of an abuse and archaic syntax, with nothing but an error message that’s as clear and useful as a lead-filled balloon used as a flotation device. It’s impossible to get a pleasing, mathematically-sound model of your domain in C++. Heck, you can’t even model something in an OO way whilst following the literature on that. Almost all effort is consumed in attempting to appease a persnickety compiler.
Since getting the syntax and the ceremony right is so much of C++, it turns into a guess-and-check game, where the student keeps tweaking things until it works. This is not the way. You don’t learn anything about why things are the way they are. This is similar to one argument I’ve heard about how Java suffers from a similar problem. We shouldn’t be teaching students how to solve programs in a given language. Rather, we should be giving them tools to think about the problems they face and how to solve them.
(apply + essay)
The thing that set me off from that lecture was that the instructor was suggesting students use pass-by-reference in function calls without even mentioning the headache that can come from side-effects and breaking referential transparency in functions. It’s the kind of thing that a beginner doesn’t need to know to program, but misuse can lead to some really nasty bugs. Anyway, programming with mutation is better avoided—best not to encourage techniques that students will have to unlearn when they encounter a pure language.
To sum up, I think the best way to start out thinking about programming is by considering how to model problem domains as best as possible, and functional languages give you the most and best tools to do that with. OO is an improvement over imperative programming, but do not use C++!