Can We Code Without Comments? //maybe
I’ll admit I don’t like writing comments. I’m not really sure why. Sometimes
I’m in the middle of some code that could use a comment and I’ll start writing
the comment, but once I finish I end up deleting it because I’m not happy with
what it says. I think I don’t like trying to explain in the imprecise language
of English what I’ve just finished explaining in the precise language of code. In this post I’m not going to debate the value of comments except to say that if the same ideas can be explained clearly in code without the need for
extensive comments, this is usually preferable. Our compilers can’t yet check that our code does what our comments say it does. Anyway, my goal here is not to write something polemical. Rather, given my aversion to writing comments, I’ve collected a few alternatives that I use in my code and I’d like to share them. In the right circumstance, I think some of these options can be strong alternatives to comments and provide good tools to reach for before resorting to using a comment to try and help with some unclear code. Since comments will never be tested by your compiler or programming language, I think it is best to make sure you explore all the options available in your programming language that can substitute for or augment a comment.
This is my favorite, so I’m listing it first. Logging is a nice combination
between English and code. It is sort of like a comment that gets executed. So
instead of writing a comment like this.
// Now we are going to convert Thing-Amajig-A to Thing-Amajig-B
We can just emit the same information as a logging statement.
logger.info(“Now we are going to convert Thing-Amajig-A to Thing-Amajig-B”)
Adding these sorts of logging messages has a side benefit of helping immensely when there is a production issue. When your service is stuck and there is no output in the logs, it can be tricky to figure out what it is stuck doing. If instead, the logs in your service say I’m going to fetch this piece of data from an external resource and that is the last thing in the log, you can go
check if you need to add a timeout there in the code.
Things to watch out for with logging is that you can easily go too far and your
service will be like my 4 year old telling me every time she is going to go
potty. Here though, most logging frameworks provide an easy solution where you assign priority to the log statement — warn info debug trace etc. — and at
run time you can decide which level of logging you want to see. Most logging
frameworks even go further by allowing you to set the level of logging message you are interested in on a per-module basis, allowing you to say that I want to see trace logs from one part of your code base and only warning or error logs from another section. Configuring these logging levels can ideally be done at runtime, so in the event that you have a production issue you can lower the logging level in the section of code where you suspect a problem. In this way, you can get visibility into your code in production of the sort that would normally require attaching a debugger. These advantages are pretty easy to realize, so next time you find yourself writing a comment explaining what the next step in the code is about to do, consider if it could be simply converted into a log message.
Testing — “Show, don’t tell”
In writing, “Show, don’t tell” is good advice and I think it applies to tests
in programming. Sometimes, I start to feel uneasy trying to explain what a
function is doing and not quite getting it right. Here, reaching for a test is
a great solution. Rather than say, this functions takes some stuff structured
exactly this way and puts out some other stuff structured a different way
according roughly to these rules, using a test, we can show direct examples of
what goes in and what comes out of your function. Tests of course have a big
advantage over comments — they are tested. Your testing framework will not
allow you to pretend a failing test is passing, but your compiler will be happy
to compile your code with factual errors in the comments. Usually, I find it
easier to write a test to explain what some tricky code is doing than to try
and explain it in English. If it is hard to understand in code, it will often
be harder to understand in English.
Why does it seem like a certain section of code needs a comment? Maybe the
first thing to do is make sure the name of the function or variable is the best
that you can do. In an ideal world, all names would perfectly capture the
purpose of the thing that is named, but we don’t live in an ideal world and
sometimes a better name or a refactor to tease apart the conflicting
functionality that is defying a clear name is out of the question. Still it is
usually worth a minute or so to wait and see if a better name pops into your
head before jumping in with a comment to explain what a certain poorly named entity actually represents.
Separation of Concerns
Related to good naming, does your code need comments because it is trying to do too many things? Before explaining what it is trying to do, see if it is easy
to split out the competing functionality. The same caveats apply here as for
above with good naming, we don’t live in an ideal world where all code will
have a single purpose and sometimes we just won’t see the obvious refactor.
Still, improving the clarity of the code is always worth striving for and
certainly worth a minute or so of thinking before trying to paste over the
issue with a comment.
Here I’m delving into territory that will depend a lot on what language you are writing in. At AppsFlyer, we write a lot of our code in Clojure, so some of
what I will discuss will be specific to Clojure, but I’ll try to be as general
as possible here.
By “specification”, I’m obviously thinking of Clojure Spec, but I’m also
thinking of many other features that are common to languages other than
Clojure. For instance, are you using the data type that most clearly expresses
your intent. A good example here is a Set vs a Vector. If you care about the
order of items in the collection then Vector is a good choice. If you don’t
care about the order and you don’t want any duplicates, then Set is a good choice. What if you care about the order and you do not want any
duplicates? Many languages offer a sorted set that might serve your use case.
Along the same lines, if you are working in a strongly typed language, there is
no need for you to add documentation saying that the first argument needs to be a string and the second one a number. This sort of thing is clear directly
from the function signature. If you are not in strongly typed language what
are your options? A straight forward option that should work almost anywhere is to express in code the requirements or contract of a function. Contracts style programming can be as simple as writing code at the beginning or end of a function to check that the arguments and returns are correct, however many programming languages including Clojure have some native support for this style of programming. Using a contract style, instead of need to write in a comment that the first item must be a string, this requirement can be spelled out directly in code.
Clojure Spec takes these ideas and builds on them. Using a rich and open system, you can specify the allowed arguments to your functions, including constraints between the arguments. For instance you can say that the first and second arguments both need to be dates and that the second date needs to be after the first one. Furthermore, you can also express constraints between the input arguments and the outputs of your functions. Once you’ve done this specification work — which is not exactly easy — in many cases you get automated testing for free. Since Clojure now knows what input types are
allowed, it can create random inputs for your function and can check that the
function outputs adhere to the specification you wrote. Many of the
specifications themselves are quite easy to read — think of predicates like
`number?` or `string?` and you can write your own predicates for application specific entities. Once you have a specification for your function, constraining what type of arguments your function takes and how those arguments relate to the outputs of the function, there is not likely to be very much left to explain in a comment. Clojure Spec is currently a work in progress, but it shows a path for rigour and safety in an untyped language.
Comments are valuable in programs — I certainly know I should write more. However we shouldn’t rely on comments when there are language features like tests or specifications that can serve the same purpose, or when a comment might be valuable as a run-time log. Likewise, we should feel some trepidation when we try to use comments to plaster over poorly factored code or poor naming. There are however few language features for effectively conveying the higher level context of code. Luckily, at the level of the architecture of the system and or service, comments and documentation really shine.
Does coding run through your veins? Join us!