It’s nearly been two years since I started courses at Waterloo. I didn’t start in CS, nor did I have any programming experience to speak of. I didn’t know the computer science courses were mandatory in first year until I tried to drop them before starting. Programming wasn’t on my roadmap.
I’m sure there are some students starting courses now that are in the same position I was in. Many students start math or CS degrees at Waterloo without prior experience with programming. However, I would bet that virtually none of the incoming students have experience with Racket.
Racket is a functional, dynamically typed programming language used in intro CS courses at Waterloo. It can feel counter-intuitive and awkward, especially to those familiar with imperative languages. I haven’t ever heard of it being used for software development. As a result, it’s earned an unfortunate (and undeserved) reputation among incoming students for being impractical and, well, useless.
Racket was the first programming language I learned. I’ve had lots of experience with it, both as a student taking CS 135 and as a tutor helping others take first-year CS courses. It was a big part of what made me decide to switch into computer science. I want to spend some time talking about why Racket is a great language and why I believe that learning it can help make you a better programmer. Students coming to Waterloo to take CS have wildly different backgrounds; people who have competed nationally and internationally in computing contests can take classes alongside those who haven’t yet written a “Hello World” program. It’s difficult for the CS department to develop courses that are challenging enough for those with experience but accessible enough that those without experience won’t fall behind. Each course is taught with no assumption of prior knowledge which lets everyone start at the same level. Functional programming is a paradigm that the majority of students haven’t worked with in high school, so starting everyone off at the beginning isn’t boring for experienced students.
Of course, functional programming isn’t different just for the sake of being different. In addition to offering a new perspective, it also allows for behind-the-scenes performance improvements. However, I want to step away from practical advantages for this post because those aren’t the focus of the first-year courses. Instead, first-year CS courses at Waterloo are focused on building up your fundamentals so that you can develop practical skills afterwards. This can be frustrating for a lot of people because of the pressure of finding a co-op in the summer after first year. I think that, in many cases, this is a misalignment of priorities. If you want a practical education that will teach you only what you need to know to get a job, a full CS degree isn’t the right fit. You’d be better off taking a college program or a coding boot-camp. The advantage of the full CS degree is it offers time to develop more sophisticated problem-solving techniques along with the theory that goes behind it, allowing for a depth of knowledge that alternatives can’t match. I think Racket is a great fit for this; it’s high-level and dynamic, allowing you to get your ideas off the ground quickly. At the same time, its design is conducive to teaching strong programming fundamentals that are incredibly valuable down the road.
I mentioned already that Racket is a high-level language in that memory management and other low-level processes are abstracted. You don’t know the difference between stack allocation and heap allocation, nor should you have to: these concepts are best introduced once you’re proficient in the basics. Of course, there are many languages that abstract these details, but Racket has an advantage over these: by offering multiple teaching languages that each have different features enabled, concepts can be introduced one at a time without needing to know about more complex features. Restriction breeds creativity; a limited set of features will force you to develop more interesting solutions and to think in an unorthodox way.
Two years later, I still remember the bonus question on our first assignment: calculate a student’s participation grade based on their clicker scores. The formula for clicker grades is as follows: two points are given for correct answers, one point for wrong answers, and only the best 75% of questions are taken. This isn’t a challenging problem, but the restrictions make it interesting: no conditionals are allowed. The question can be done using only basic mathematical operations. It forced us to think in a way that we weren’t used to by limiting the tools we had access to. The philosophy of feature restriction is seen throughout the courses and does an excellent job of gradually building up concepts.
I also want to discuss why the choice of a functional language to teach programming is important. I’ve already spoken about how functional programming is a paradigm that forces you to use different problem-solving strategies. But why is that? Functional programming is the idea of programming without side-effects; pure FP has no global state, mutable variables, or input/output. Rather, FP functions are a lot like the ones we see in math: for each set of inputs, we’ll produce an output. Nothing more. As a result, near-ubiquitous language features like variables and loops don’t exist in Racket. Instead, solutions must rely on recursion.
Waterloo’s first year courses emphasize structuring recursive solutions around two key components: a base case and a recursive step. This makes general case of recursion very straightforward and leads naturally to abstractions at the end of the course. The courses focus not only on solving the immediate problem but also on generalizations that apply to similar problems. When loops are introduced in later CS courses, they continue to build off of these fundamentals, allowing for more powerful abstractions (range-based loops, iterators) without losing any understanding of the underlying process. By breaking solutions down into small components that can be easily generalized, these courses develop skills that are relevant for all programming languages, not just functional ones.
Finally, Racket is powerful because it allows us to make generalizations. The problem of sorting a list isn’t just specific to numbers; lists of characters, strings or even structures can be sorted as well, as long as we can compare them. Therefore, sorting can be generalized to use a common algorithm with the only difference being a comparison function. Because it’s dynamically typed, Racket allows for solutions that don’t depend on any one type. In statically typed languages, making generalized solutions is often either verbose (templates in C++) or non-existent (Go). The first-year courses focus on generalizations by providing templates that can be used for common problem types. Racket takes this one step further by providing abstractions on any recursive problem using its abstract list functions. Teaching programmers to generalize goes a long way for promoting code that is flexible and maintainable.
The courses don’t teach any mind-blowing or overly complex concepts. Some of what gets covered can feel familiar to those who have a programming background. However, I think the real strength of these courses are their ability to present basic concepts in a new way and introduce another perspective with functional programming. Racket abstracts a lot of hairy, low-level details and the courses expose only the features needed, allowing you to develop critical thinking and problem-solving skills. Although I’ve never used Racket in an interview or on the job, it helped set the foundation for the way I approach problems. It can help you too.