Code style and moral absolutes
In my previous post about my basic setup for solving competitive programming problems with Haskell, I (somewhat provocatively) used lists to represent pairs, and wrote a partial function to process them. Commenter Yom responded with a proposed alternative that was (less) partial. I was glad for the comment, because it gave me a good opportunity to think more about why I wrote the code in the way I did, and how it fits into larger issues of good coding practices and the reasons behind them.
Good code style as moral behavior
What is good code style? You probably have some opinions about this. In fact, I’m willing to bet you might even have some very strong opinions about this; I know I do. Whether consciously or not, we tend to frame good coding practices as a moral issue. Following good coding practices makes us feel virtuous; ignoring them makes us feel guilty. I can guess that this is why Yom said “I don’t think I could bring myself to be satisfied with partial functions” [emphasis added]. And this is why we say “good code style”, not “optimal” or “rational” or “best practice” code style.
Why is this? Partly, it is just human: we like to have right and wrong ways to do everything (load the dishwasher, enforce grammar “rules”, use a text editor, etc.), and we naturally create and enforce community standards via subtle and not-so-subtle social cues. In the case of coding practices, I think we also sometimes do it consciously and explicitly, because the benefits can be unintuitive or only manifest in the long term. So the only way to get our students—or ourselves—to follow practices that are in our rational self-interest is by framing them in moral terms; rational arguments do not work in and of themselves. For example, I cannot get my students to write good comments by explaining to them how it will be beneficial to them in the future. It seems obvious to them that they will remember perfectly how their code works in the future, so any argument claiming the opposite falls on deaf ears. The only way to get them to write comments is to make it a moral issue: they should feel bad (i.e. lose points, lose respect, feel like they are “taking shortcuts”) if they don’t. Of course I do this “for their own good”: I trust that in the future they will come to appreciate this ingrained behavior on its own merits.
The problem is that things framed in moral terms become absolutes, and it is then difficult for us to assess them rationally. My students will never be able to write a [function without comments, partial function, goto
statement, …] without feeling bad about it, and they probably won’t stop to think about why.
Good code style as rational behavior
I ask again: what is good code style—and why? I have identified a few reasons for various “good” coding practices. Ultimately, we want our code to have properties such as:
- Robustness: it should handle unexpected or invalid inputs gracefully.
- Readability: it should be easy for others (or us in the future) to read and understand the program.
- Maintainability: it should be easy to modify the program as requirements change.
-
Efficiency: in general, programs should not do anything obviously redundant, or use data structures with a lot of overhead when faster ones are available (e.g.
String
vsText
orByteString
).
Even in scenarios where one might initially think these properties are not needed (e.g. writing a one-off script for some sysadmin or data processing task), they often end up being important anyway (e.g. that one-off script gets copied and mutated until it becomes a key piece of some production system). And this is exactly one of the reasons for framing good coding style in moral terms! I won’t write comments or use good function decomposition in my one-off script just because I know, rationally, that it might end up in a production system someday. (I “know” that this particular script really is just a one-off script!) But I just might follow good coding practices anyway if I feel bad about not doing it (e.g. I would feel ashamed if other people saw it).
It seems to me that most things we would typically think of as good code style are geared towards producing code with some or all of the above properties (and perhaps some other properties as well), and most scenarios in which code is being written really do benefit from these properties.
“Good” code style is context-dependent
But what if there was a scenario where these properties are actually, concretely of no benefit? As you can probably guess, I would argue that competitive programming is one such scenario:
- Robustness: we do not care what our program does when given unexpected or invalid inputs, since we are absolutely, 100% guaranteed that our program will only ever be run on inputs that exactly follow the given specification.
- Maintainability: the requirements for our program will never change.
- Efficiency: if you haven’t done much competitive programming you might be surprised to learn that we often don’t care about efficiency either. That is, although we certainly do care about asymptotic efficiency, i.e. choosing a good algorithm, problem time limits are typically set in such a way that constant factors don’t matter very much. A program that runs within 5x-10x of the optimal speed will often fit comfortably within the time limit.
So what do we care about?
- Readability: the one thing from my previous list that we do care about is readability. Debugging becomes quite difficult if you can’t read and understand the code you wrote (this becomes even more important if you’re working on a team). And insofar as a solution represents particular insights or techniques, you may want be able to read it much later in order to remember or share what you learned.
- Programmer time: programmer time is always valuable, of course, but with competitive programming this is taken to an extreme: it is almost always done under time pressure, so the whole point is to write a program to solve a given problem as fast as possible.
The combination of optimizing for speed and not caring about things like robustness, maintainability, and efficiency leads to a number of “best practices” for competitive programming that fly in the face of typical standards. For example:
-
Adding code to deal gracefully with inputs that don’t follow the specification would just be a waste of time (a cardinal sin in this context!). My Haskell solutions are full of calls to partial functions like
read
,head
,tail
,fromJust
, and so on, even though I would almost never use these functions in other contexts. This is also why I used a partial function that was only defined on lists of length two in my previous post (though as I argue in a comment, perhaps it’s not so much that the function is partial as that its type is too big). -
I often just use
String
for text processing, even though something likeText
orByteString
(depending on the scenario) would be faster or more robust. (The exception is problems with a large amount of I/O, when the overhead ofString
really does become a problem; more on this in a future post.) -
Other than the simplest uses of
foldr
,foldl’
, andscanl
, I don’t bother with generic recursion schemes; I tend to just write lots of explicit recursion, which I find quicker to write and easier to debug.
There are similar things I do in Java as well. It has taken me quite a while to become comfortable with these things and stop feeling bad about them, and I think I finally understand why.
I’m not sure I really have a main point, other than to encourage you to consider your coding practices, and why you consider certain practices to be good or bad (and whether it depends on the context!).
Next time, back to your regularly scheduled competitive programming tips!