Ronkainen Rants: To Use a Hammer to Carve a Boat

Contents

  1. Introduction
  2. Linked lists (Teaching the wrong tools or ancient lore)
  3. The Big O notation (Overfocusing on one aspect without considering the others)
  4. Teaching something is bad without giving the students the tools to understand why
  5. Object-oriented programming (Stuck in the trend of the day)
  6. Pointers (Teaching things too early)
  7. Encapsulation (Overusing a tool to the point of stupidity)
  8. Dismissal of the tools the language offers
  9. Using the wrong language as a teaching medium
  10. So why is everything the way it is?
  11. What can we do about it?

Introduction

I have an issue with how programming is often taught. Even more so at higher education levels and in those Internet tutorials that promise you the moon and deliver at best a grain of sand.

Why? Because they teach you how to use the back of a hammer to carve a boat, or in a worse case, they teach you how to graft trees around a mould, so the tree grows into the shape of a boat. Which, as you might have figured out, are perhaps not the best approaches to getting a good, functional boat done.

This is somewhat a pet peeve of mine, and a partial reason for me to join Buutti, since Buutti was the first place where I saw an actually decent C++ course. (I hope that when I got to give the lectures, I didn’t let people down)

Also, note that I mostly use C++, C, Python, and Rust (in that order) as my tools of choice, which affects my point of view considerably. The things I’ve observed might not be as bad for some languages as they are for others, especially for those that offer you a much more limited toolset. In any case, my examples mostly refer to C++, but the ideas should be applicable to teaching any programming language.

It unfortunately isn’t even too rare an occasion when the actual professor has zero clue what they’re talking about.

It’s not a singular occasion when I meet someone straight out of education with good grades. But while they know how to use the tools to a certain degree, they often do not understand when those tools should be used. It unfortunately isn’t even too rare an occasion when the actual professor has zero clue what they’re talking about. You’ll likely come to witness this if you get far enough into the land of learning the ins and outs of a programming language.

I’ll go through a somewhat randomly chosen list of issues generated by my brain at the spur of the moment.

Linked lists (Teaching the wrong tools or ancient lore)

Linked lists hold a special place in my heart. In the bygone times, I’ve heard it was common to write an implementation of one yourself. I’m not exactly a new programmer, but even for me, the only reason I’ve ever written a linked list is because I thought it would be useful since it was taught in so many places. There has been exactly one time when I actually needed one, and that was because I absolutely needed stable addressing. Linked lists are almost always the wrong tool for the job, so do not go into teaching them as the first container type.

When I applied linked lists for the first time since everyone and their dog were talking about them, and gave it a little thought and some performance measurements afterwards, I realised I had spent time to pessimise my code. It took me some time to realise why that happened since I was not too familiar with how CPU caches worked back then. But the thing is, even if linked lists were “fast to insert and delete from the middle”, they lost to a more costly alternative in almost every benchmark because there weren’t millions of elements.

There is another container that is not too difficult to write and I’ve actually needed to write multiple times for different cases. And that is dynamic array, or vector if that is a more familiar name for you. If I absolutely had to teach how to create a container for data to a beginner, I would rather spend the time teaching to implement something that is actually useful. Implementing vectors also gives more insight into problems that are more commonly faced elsewhere, instead of something like cyclic references (which I’ve almost never seen in practice) or other mostly-academic issues.

But now that we got started talking about performance, let’s talk a bit about Big O notation and how that gospel isn’t what it might first seem like, either.

The Big O notation (Overfocusing on one aspect without considering the others)

The most common performance notation you’ve probably seen is the Big O, and don’t get me wrong, I think it is useful — as long as you understand what you’re approximating with it. But if I got a coin every time I showed someone who has a degree in computer science that for our use case I could write a faster algorithm with worse Big O notation and proved it with benchmarks, I would have not that many more coins, since I would’ve spent them, probably on beer.

The problem comes up when only Big O notation is considered in evaluating an algorithm without really paying attention to how the hardware the algorithm is run on actually works. I’ve repeated to the point of absurdity that CPUs are much faster at processing data than retrieving it from memory. And this is often much more critical than asymptotic analysis. We rarely need to have good asymptotic performance characteristics unless we really do have an arbitrarily large dataset. And what more often becomes critical is good cache locality.

Teaching Big O usually isn’t technically wrong, and it is actually helpful in many cases, and it should be considered. But it should be made clear that it shows what kind of performance can be expected with arbitrarily large datasets, not in general, or application-specific cases. And that most often there are other, more important factors to consider.

Teaching something is bad without giving the students the tools to understand why

There are two things we all know are horrible, and those are gotos and global variables. I’ve heard postdoctoral professionals still say “We shouldn’t use globals, ever. Because they are evil.”

The common reason stated to avoid gotos is that they make your code difficult to follow. It can be true, but most people do not realise that these statements originate from a time when things such as function calls were a new thing.

Back then, gotos were not structured, and you could jump pretty much anywhere in the code. And seeing how this makes goto horrible should not be very difficult to see in the modern day.

In modern languages (and I consider C modern here) that have goto, you can’t jump around arbitrarily anymore. But the stigma stays.

And while we talk about global variables, we might as well take a look at something I hate with a passion, but let’s first see what the downsides of global variables are. There are three significant ones that we should consider.

– They may introduce a global state to the program
– They may cause naming collisions
– In languages with objects, their initialisation/destruction is not explicit and may be unordered

These are real problems, but there are solutions too. Most languages offer translation-unit-level privacy or namespacing, either directly or with modules, which remedies the second point.

Lazy initialisation is the tool to solve the latter problem when necessary. Rust, for example, enforces that approach. Usually you also only need to write lazy initialisation code once, and it would work for everything even if it wasn’t provided by the standard library of your language.

This leaves just the introduction of a global state as the problem. At some point, Java became a thing, and since you didn’t have globals in Java, they came up with the horror that is the singleton pattern. Which, in my not-so-humble opinion, is way worse than either goto or global variables.

For reference, a singleton object is an object that can be instantiated only once. Or in other words, it is an object that represents the global state. So, we haven’t gotten rid of the first problem of global variables with that. Since it’s a class, it does get rid of the naming collision problem. It also requires some way to construct the object in the first place, so you’ll need to make a way to initialise it lazily. And you are usually encouraged to do this for every singleton object separately.

But for some reason that I cannot comprehend, this is taught and global variables are vilified, even if they are essentially the same. Except that with singletons you have to do more work, and are left with worse syntax for the end-user.

The reality with both goto and global variables is that there are legitimate use cases for both of them. Sometimes your program just has to deal with a global state, and you’ll need a way to represent that state.

Sometimes goto just does the job. Some examples would be effective jump tables, implementing coroutines or in some languages, multi-level breaks (though if you need the last, you likely have a problem with your architecture).

Don’t create dogma. Teach what and why, so people can make their own decisions when to use the tools they have at their disposal.

And that leads us to…

Object-oriented programming (Stuck in the trend of the day)

Programming is not immune to trends. People get excited about something, they like it, endorse it and it spreads. Then it gets overused and applied to way too many things, and people start to realise that “maybe this wasn’t the best idea after all”.

Object-oriented programming has gotten to the last stage of that chain. It was the rage at some point, in a somewhat similar way functional programming is touted everywhere currently, albeit to a much lesser degree. OOP is not a new thing, with Simula being the trope codifier in the 1960s. Plenty of languages introduced classes, objects, inheritance and dynamic binding to their repertoire because those things made reasoning around complex software much easier.

Then at some point, people forgot that the purpose of programming language features is to reduce the cognitive load for the programmer. OOP changed from object-oriented programming to object-obsessed programming.

In the late ’90s and early 2000’s everything had to be object-oriented, and the more complex structures you got away with, the better. And some of this attitude still shows when looking at C++ or Python code today.
We went from having reasonable objects, such as message, queue or grid to MessageQueueManager or GameStateManagerFactory because everything just had to be a class, whether it was a sane choice or not. There still is a persistent idea that putting stuff to a class gives it some extra magical property of “better”, even when all it does is to make your code more complex with zero technical gain.

There still is a persistent idea that putting stuff to a class gives it some extra magical property of “better”.

And this is a big problem for the teaching curriculum. We get so stuck to the trend that we start to think it should be applied everywhere, and trying to hype up different paradigms and apply them to practice without ever considering where and when they should be used.

The worst part for me personally is that OOP is usually taught with languages like C++, which is a multi-paradigm language, not an object-oriented one. This misleads people to think that using big classes everywhere is a good way to use that language. For more experienced programmers, it just feels like somebody is trying to press a hammer to a screw so hard that it could be used as a screwdriver from sheer friction alone.

Here’s an actual protip: If you are having difficulties naming a class, and you are using a language where using classes isn’t mandatory, the thing you are making should probably not be a class. 

Pointers (Teaching things too early)

Somewhat related is the tendency to teach some things way too early to new programmers because they were important basics to know 30 years ago. The best example of this I can think of is pointers.

When the common case was to be working with lower-level languages, we needed to think about different concepts than with higher-level languages. If we’re writing C, sure, we do need to learn how pointers work. And if we’re dealing with asm, we actually need to know how pointers work. But that is not the common case anymore, yet teaching pointers is still stuck at the 101 courses for programming. I’m going to go out and say that it is actively harmful.

Even with language such as C++, where the goal is to “leave no room for a lower-level language”, I teach pointers mostly because the industry expects it, not because I believe it would be useful for the students for a good while. If I taught somebody who just wanted to learn the practical use of the language, I would leave pointers to the far future. (That does not hold for references, those I would absolutely teach during the language introduction course!)

And from there, I’ve concluded that there are only two reasons to teach pointers in any beginner-level programming course. The first one is that the industry often expects you to know these, and they pop up in job interviews. Which is, from a technical standpoint, a pretty stupid reason.

The other, and actually good reason to teach pointers to any beginners programming course is that you’re teaching embedded programming and have the explicit need to get access to a device through memory.

Barring that, and outside C, there is hardly any junior-level programming jobs that would ever require using pointers, unless dealing with legacy code. And I quite frankly think that teaching them at the entry-level courses is a historical remain that should just die. They are an advanced concept, and you can write years’ worth of code without ever touching one. When was the last time you explicitly needed the memory address of something?

Yet it is taught early, as a basic thing to do in many places. Which leads, again, to people using it instead of the tools they are given by the language and its standard library. And these same people usually do not understand what heap allocations are or have the experience to debug and handle memory errors they are subjected to.

But pointers are an implementation detail, unless you explicitly need to expose memory addresses, which leads us to…

Encapsulation (Overusing a tool to the point of stupidity)

I think encapsulation is the most overused tool in the toolbox.

The main job of encapsulation is to hide implementation details, i.e. the things that the user of the class/library/code does not need to care about.
But then I see horrors like this:

class Point {
    public:
        int     getX() { return x; }
        void    setX(int value) { x = value };

        int getY() { return y; }
        void setY(int value) { y = value };

    private:
        int x;
        int y;
};

Which is a direct result of the sometimes taught idea of “anything that can be private should be private”. Which, I feel, is equivalent to saying that “everything that can be hit with a hammer should be hit with a hammer”.

The dismissal of why here is the reason this kind of insanity sometimes appears in codebases. You want to hide implementation details, but for something like a Point, x and y are not implementation details, and thus, all that is accomplished here is that the code is now more difficult to read and use.

You have to wonder if the getX and setX do something extra at the call site. It is highly likely that if those ever change, you’re going to introduce bugs to your code, either because something relied on the old behaviour or because some poor programmer checked it before, but since it now doesn’t work in the way it used to, some new error pops up.

How to do it right, you ask?

class Point { // I would in real code just use struct here, but I
    public: // use class for consistency with the previous code
        int x;
        int y;
}

Congratulations, it works, and its usage is clear from the call site without jumping around to check the getters and setters. (Which, somewhat ironically, is near the exact same “jumping around”-problem that gotos get called out for)

This is the problem of teaching tools without teaching why they are used. And if this section got your neurons fired up in your brain, now piling up to refute all of this; now you are thinking about why. Teach that instead of teaching the catch-alls without comprehension.

Dismissal of the tools the language offers

When I had used C++ for a couple of years, I still thought that “yeah, learning C first is a good step to learning C++”, and I might’ve still recommended that approach (what a fool I was!).

The more I’ve learned and thought about it, the worse idea it seems to me. But that is because I became somewhat more active in following the C++ community. My code worked, I could write stuff reasonably quickly and with a reasonably small error count. Nobody complained about my code quality — at least to my face. I could’ve remained in the comfortable illusion that I was good with the language for a good while. Probably for the rest of my life if I would have been in a well-paid job and had no incentive to get better.

But at that point, I had no idea how many bad habits I had gathered along the way. I was still happily mallocing around and playing with void pointers (a pointer to some arbitrary data, for the non-C-people), along with many more C-isms in my code. So of course I thought that coming from C was helpful.

Later I realised that there are plenty of tools at my disposal provided by the standard library. Smart pointers or hash maps weren’t yet in the C++ standard library at this point, but a lot of other useful things certainly were, and back then boost had a lot of the stuff that would later become part of the standard. But people do not use tools they do not know exist.

And an even worse problem is, there are people still teaching those C-isms and dismissing all the tools provided as a side note at best. It’s like thinking that programming hasn’t gone forward in the last 40 years. From dismissing the use of extremely common features to straight up teaching positions advocating against the best practices .

This kind of insanity is what I meant when I said that it is not too rare of an occasion to see university teachers teaching something they have zero clue about. I don’t know if it stems from general ignorance, “I’ve always done it this way”-attitude or what, but this is one of the more damaging things teachers can do to their students. If you teach a language, especially at the university level, you should know your language and keep up with its development. I know it isn’t the least time-consuming thing, but if you, as a teacher, are not up for that, you should at least make it clear to your students.

Often this is dismissed by “just teaching a concept and using <language> to do it”, but that leads us to…

Using the wrong language as a teaching medium

This is a devious one, because its effects do not become apparent until after people have graduated and start writing for a project using a certain language.

They have learned habits from their classes, they now wrap everything into a class in Python, malloc and new like there is no tomorrow in C++, and do not know when to implement something themselves and when to let the language do their job for them since that was the way they used their language the first time.

A good practice in one language might be a really, really bad idea in another, if we keep using the wrong language to teach a concept, those concepts will stick even if they were taught just to demonstrate a concept. But teachers need to realise that they are not just teaching that concept, they are also teaching the habits of the language they use as a demonstration tool. This is especially true for people who haven’t had the programming experience from their hobby before they came to the world of education to learn.

A good practice in one language might be a really, really bad idea in another.

Using a different programming language for demonstrating different concepts has the additional benefit of making student accustomed to multiple different languages. There’s a persistent thought that learning different programming languages is somehow hard, but most experienced programmers have worked in several, and it’s good to show how similar different programming languages really are, so that students aren’t afraid to try out new ones.

Using the wrong language as a teaching medium is also something that makes it more difficult to see why the concept being taught is useful, which in turn steers toward all the other points I’ve brought up.

There’s also a milder version of this, albeit that’s probably more annoying to the students. In that scenario, we are teaching programming using a language, but the tasks used for teaching are something the language completely sucks at. E.g. C for string manipulation. We don’t teach brainfuck for web programming. Why are we doing this?

I think it’s less devious than the previous issue since it’s immediately obvious even to an amateur that doing this sucks, and they are very likely to figure out nobody actually does that in the real world unless they have some extremely compelling reason. But if it makes someone think that all programming is like that, that is a pretty big fail in my book.

So why is everything the way it is?

I think the core of the problem is that people are using tools because they are told to. And then those people teach that forward without ever grokking the complete picture. This is then augmented by many courses having picked the wrong tool — or a language — to present a problem and its solution.

Another thing is seriously underestimating the power of habits. Teachers often take shortcuts they shouldn’t take. From using namespace std to shorten your code in slides to teaching stuff in the wrong order to go through things in the right order from “bottom to top” (which never works because there isn’t a bottom level). If you teach something like this, it will find its way to production code, and it will cause actual damage, most commonly in the form of lost work hours trying to unravel what somebody tried to do.

Languages invent features, and standard libraries exist in order to reduce cognitive load. Way too rarely do people stop and ask themselves if what they are doing actually manages to simplify some aspect of their code. There are way too many teachers who go along the lines of “this is how it was done in my youth!” and end up teaching obsolete ways or universally panned habits.

Programming languages themselves also have the nasty habit of evolving fast, which is why it is even more important to understand the whys and not just repeat what somebody else has told you. Even in C++ and C, for which the pace of progress is often described as glacial move forward faster than the teaching materials follow.

What can we do about it?

As a teacher

One thing I can think of is that teachers should follow the development of the language and the toolset they are using to teach. I know it takes considerable effort to do so, and there are many of you who go to the lengths and do that, and if you are one of those people, my sincerest thanks for trying to keep things sane.

But the more manageable and important thing is simpler. Just teach why and when as well as how. Keep an open mind to changes in the languages, and try to understand how different languages and tools serve different purposes. And don’t get complacent.

Another thing we need to remember when teaching is that anything we teach that people have to unlearn later, is something that makes the learning process significantly harder. And this applies, whether the goal is to make it easy for people to actually understand the concepts or give them the tools to use in practice.

When teaching, do not take shortcuts that can give the wrong impression. It is really simple to lapse into “I’ll just cut this out, it’s obvious”, but nothing should be considered obvious to people just starting. Even if they get it right, they shouldn’t need to spend extra effort just because we’re too lazy to do so ourselves. It’s our comfort versus their understanding of the topic at stake.

As a student

Do not literally copy-paste code

By all means, go to Stackoverflow, see the result and write it verbatim to your code if you want to, but do not CTRL+C CTRL+V it into your code. Rewrite it and try to understand each line of the solution you are writing.

Be critical. If you’re still learning, you don’t have the expertise to discern how accurate the lessons you are taught are. Ask yourself and your teacher “why do this instead of that?” You won’t always get a satisfactory answer, but at least you might get more insight into the issue at hand.

Participate in a real-world-project with other people. It doesn’t have to be large, special, or even functional in the end, but programming is more often than not a group effort and getting some practice is always helpful for learning. If you find an open-source project you use and wish it would have some feature or a bug that annoys you, try contributing. Even somewhat widely used open-source projects are undermanned nearly always, and even if your contribution doesn’t get accepted, you will usually get good advice on why someone wouldn’t want to use a free contribution you offered when they are already undermanned. If that happens, it should ring some alarm bells, but in any case, you are more likely told what to fix and sometimes even how to do it, much like in a company-internal code review. Don’t be afraid to show your code because you think it’s messy or if it’s just a couple of lines of fixes.

Don’t use examples from competitive programming (such as leetcode) as your guide. They are valued by very different criteria than real-world production code is. Competitive programming is more “fire and forget” than “this code needs to be fast, but it also needs to be maintainable for years”. It doesn’t teach you good habits.

As either

Take part in the community. Most programming languages have at least moderately active communities, which usually are very knowledgeable about their tools of the trade. You’ll slowly pick up good habits and just seeing what things are controversial and which are criticised or outright banned in other people’s projects gives you insight that’s hard to get elsewhere.

Try out different things. Learning to see things from different perspectives is far too undervalued skill in programming.

And try to make it fun, for both yourselves and the others around. We are working in one of the more mentally taxing jobs, at least if burnouts are any indicator. We should aim to make things easier for each other and share our skills forward.