Theory and Practicality

“Theory” is opposed to “practicality” in programming, right? This is a standard jumping off point for self-description (“I just like to get things done”), pejorative terms (“architecture astronaut”), office gossip (“He thinks about 'patterns' too much”), and even books and professional movements (“software craftsmanship” as opposed to “applied computer science”, perhaps).

All of this is based on an unfortunate thread of misunderstanding that runs throughout the software development trade: it is an anti-intellectual rejection of the fact that our job is fundamentally one of ideas. Every tool that we use, and in fact all of the code that we write, is founded on theories, and understanding of theory is a prerequisite for effective programming.

Now, this is not to say that all people who call themselves “practical” are dumb and ineffective. Nor is it to say that everyone who reads “pattern” books is smart and effective. Instead, understand that every advancement that we have made is on the back of theory, and making yourself aware of that—and actively seeking to understand the underlying cause for certain things—can make you more effective as a programmer and as a technical decision-maker.

For example, consider time complexity. This is a simple, first-year computer science concept, which is nevertheless disregarded by many working programmers: an inefficient algorithm which grows exponentially with its inputs can never be redeemed by more powerful computers. Operating on a list of a thousand elements, let's say that an \(O(n^2)\) algorithm would take 10,000 time units on an old computer; a newer computer, ten times faster, would take 1000 time units. Meanwhile, an efficient \(O(n)\) algorithm would take 1000 time units on the old computer. You'd think this would be received wisdom at this point; there are even convenient online references for many algorithms. however, I have dinged inefficient nested loops numerous times in real code reviews for real code bases.

Then there's latency. Like time complexity, this is well understood and has lovely interactive graphics. It should be received wisdom, right? Yet how often do people make huge queries from the database and filter the result in memory? How many times have you caught an n + 1 error in a code review? No matter how fast your database is, an n + 1 problem leads to numerous completely unnecessary round-trips across the network, and can frequently add seconds to a web application's response time. That should never be written, let alone checked in.

What about failure? There are two primary modes of failure that most people must deal with: exceptions and nulls. Both are problematic and are very seldom contained properly. In mainstream languages, you can view all types as being inhabited by null, and returning this “universal value” is frequently used in standard libraries and popular frameworks as shorthand for “no result” or “an error occurred.” Any time you call an external API, you should be worried about receiving null and must do everything you can to contain that case. Tony Hoare calls the null reference his “Billion Dollar Mistake.”

Exceptions, on the other hand, constitute a parallel type structure. If a function can return a value of a given type, or a null reference, or throw a checked exception, or throw a runtime exception, there are essentially four paths that might be relevant for any given line of code. I have seldom reviewed code that I felt confident was robust in all four scenarios; nor, in most languages, can I write code that makes me confident that I have successfully dealt with these four possibilities. And yet, and yet—there is resistance to learning and using techniques that make it easier, like Railway-Oriented Programming and Option types.

Even when it comes to problem modeling, where “fuzzy” rules reign in the everyday practice of programming, theory offers hard and fast rules which programmers should strive to follow. For example, a function or method should do only one thing. If it's calculating interest, it should not be calling a web service to retrieve the rules for a given locale. If it's modifying the DOM, it should not be calculating interest. This is the single responsibility principle, and it's practical (testing and reasoning about code is easier) as well as theoretical. Similarly, classes (in the object-oriented sense) should do the same things if they are intended to serve the same role. This is a restatement of the Liskov substitutability principle, which presumes subtyping, but the theory behind that idea is really far more general: don't surprise yourself. If there is a VisaCreditCardProcessor and a DiscoverCreditCardProcessor, they had better do exactly the same things behind the scenes, or you have written yourself a time-bomb.

Is there a way for this to be a pithy phrase? Maybe not. But I'm going to try.

tl;dr

It's fine to consider yourself practical, a craftsman rather than a theoretician. However, remember that the goal is solving a problem, not producing code. Are you giving yourself the tools you need to make sure that your solutions are as efficient, effective, and comprehensible as they can be?