DEV Community

Cover image for Code Comments Are Stupid
Mat Jones
Mat Jones

Posted on • Updated on • Originally published at mjones.network

Code Comments Are Stupid

Code comments are lame.

Code comments once served a critical purpose; adding context or explanation to a snippet of code. But this was back when programming languages were mostly esoteric, and the barrier to entry for writing code was extremely high.

Today, though, we have extremely expressive programming languages, like C# (especially since the addition of LINQ), Python, Kotlin, etc. that make code comments virtually obsolete. With a highly expressive language, nearly all comments become unnecessary, because it’s easy to write self-documenting code; the code is easy enough to understand at a glance that you don’t need comments anymore.

“You should always write your code as if comments didn’t exist.” — Jeff Atwood (source)

I’ll be writing most of my code examples in Kotlin because it’s one of my favorite languages, and I haven’t gotten to use it recently, but all these examples are applicable to any sufficiently expressive language.

Implementation Comments

Most of the time, comments in the implementation of a bit of code can be made unnecessary just by naming things better, and keeping your functions and methods small and focused on a single task. Let’s consider a simple example.

We’re working on a web application, and we want to implement a method to validate that a password meets certain required criteria. For this example, let’s say our criteria are:

  • Must contain at least one uppercase letter and at least one lowercase letter
  • Must contain one number
  • Must not include whitespace
  • Must contain one of the following special characters: ! # $ % ^ & *
  • Must be between 8 and 20 digits

We could do it in very few lines of code with a complex regular expression:

/**
 * Validate that the given password matches our password criteria.
 * @param password the password to check against our criteria
 * @return         true if the password matches all criteria, false otherwise
 */
fun isPasswordValid(password: String): Boolean {
    // Pattern matches that:
    // - Must contain at least one uppercase letter and at least one lowercase letter
    // - Must contain one number
    // - Must not include whitespace
    // - Must contain one of the following special characters: ! # $ % ^ & * 
    // - Must be between 8 and 20 digits
    var pattern = """^(?=.*[A-Z])(?=.*[a-z])(?=.*[0-9])(?=.*[!#$%^&*])[\S]{8,20}$""".toRegex();
    return password.matches(pattern);
}

fun main() {
    println(isPasswordValid("Passw0rd!")); // true
    println(isPasswordValid("P assw0rd!")) // false; contains whitespace
    println(isPasswordValid("Password!")); // false; no numeric digits
    println(isPasswordValid("Passw0rd")); // false; no special characters
    println(isPasswordValid("password!")); // false; no uppercase letter
    println(isPasswordValid("PASSW0RD!")); // false, no lowercase letter
    println(isPasswordValid("Pswrd!")); // false; too short
    println(isPasswordValid("UltraL0ngSecurePassw0rd!")); // false; too long
}
Enter fullscreen mode Exit fullscreen mode
A great way to make enemies is to make sure nobody else can understand your code.

Without those comments inside of the isPasswordValid function, it’s really cryptic and almost impossible to tell what it’s doing without taking a ton of time to figure out that really complicated regular expression.

By splitting each password criterion into a distinct unit of work, the code and the regular expressions used in the code both become a lot clearer and easier for a reader to grok with just a glance.

fun containsUppercaseLetter(str: String) = str.matches("""(?=.*[A-Z]).*""".toRegex());
fun containsLowercaseLetter(str: String) = str.matches("""(?=.*[a-z]).*""".toRegex());
fun containsNumericDigit(str: String) = str.matches("""(?=.*[0-9]).*""".toRegex());
fun doesNotContainWhitespace(str: String) = !str.matches("""(?=.*[\s]).*""".toRegex());
fun containsSpecialCharacter(str: String) = str.matches("""(?=.*[!#$%^&*\.]).*""".toRegex());
fun lengthIsInRangeInclusive(str: String) = str.length in 8..20;

/**
 * Validate that the given password matches our password criteria.
 * @param password the password to check against our criteria
 * @return         true if the password matches all criteria, false otherwise
 */
fun isPasswordValid(password: String): Boolean {
    return lengthIsInRangeInclusive(password) &&
            containsUppercaseLetter(password) &&
            containsLowercaseLetter(password) &&
            containsNumericDigit(password) &&
            doesNotContainWhitespace(password) &&
            containsSpecialCharacter(password);
}

fun main() {
    println(isPasswordValid("Passw0rd!")); // true
    println(isPasswordValid("P assw0rd!")) // false; contains whitespace
    println(isPasswordValid("Password!")); // false; no numeric digits
    println(isPasswordValid("Passw0rd")); // false; no special characters
    println(isPasswordValid("password!")); // false; no uppercase letter
    println(isPasswordValid("PASSW0RD!")); // false, no lowercase letter
    println(isPasswordValid("Pswrd!")); // false; too short
    println(isPasswordValid("UltraL0ngSecurePassw0rd!")); // false; too long
}
Enter fullscreen mode Exit fullscreen mode
A few more lines of code can go a long way for clarity.

The code is only a few more lines long, but is so much clearer at a glance that we don’t even need that huge comment block anymore. We’ve converted a comment telling us what the code is doing into code that tells us itself what it’s doing.

Redundant Comments

This is the most useless (and annoying) type of code comment. These are the type of comments where you can just remove the comment without even changing the code, and it’s still just as clear what the code is doing.

Let’s say we’re working on a command line tool grab some data from an API and write it to a file. We’ll need to ask the user for a file path to write the data to.

fun callApi(): ApiResult {
    // implementation omitted
}

fun writeJsonToFile(json: String, filePath: String) {
    // implementation omitted
}

// Call the API and get the results
var apiResult = callApi();

// Get the desired file path to save the results to
print("Enter file path to save results:");
var filePath = readLine()!!;

// make sure it's a JSON file
if (!filePath.endsWith(".json")) {
    filePath = "${filePath}.json";
}

// write the data to the json file
writeJsonToFile(apiResult.toJson(), filePath);
Enter fullscreen mode Exit fullscreen mode
Seems good… or not?

There’s a lot cluttering up this code, though. It may seem a bit complicated, but that’s just because of the comments. In situations like this, removing the comments can actually make the code less intimidating to a reader.

When you see a function defined as writeJsonToFile(json: String, filePath: String), with no comments or explanation, what do you think it does? You can probably immediately understand that “it writes some JSON data to a file”. We can remove every single comment, and the code is still just as clear, readable, and understandable.

fun callApi(): ApiResult {
    // implementation omitted
}

fun writeJsonToFile(json: String, filePath: String) {
    // implementation omitted
}

var apiResult = callApi();

print("Enter file path to save results: ");
var filePath = readLine()!!;

if (!filePath.endsWith(".json")) {
    filePath = "${filePath}.json";
}

writeJsonToFile(apiResult.toJson(), filePath);
Enter fullscreen mode Exit fullscreen mode
Its still just as easy to understand this code without the comments.

We didn’t change any of the code here, we just removed all the comments. Yet, it still seems less intimidating on your first read, right? Appropriately named variables and functions make all the comments completely redundant, so the comments were just clutter.

Exceptions to the Rule

Documentation Comments

The main exception to this is tooling-specific documentation comments, such as JSDoc and C# XML Documentation Comments. These types of comments provide popup API documentation via IntelliSense (or another autocomplete mechanism) in most IDEs and editors, like VS Code, Rider, and others.

Documentation comments also usually hook into documentation generator tools, like DocFX or Sandcastle, which can automatically generate HTML documentation web pages from your documentation comments.

Clarification Comments

Sometimes, there is an obvious solution that seems simpler or better, but there is a specific reason the developer chose not to use that solution. I’ve found this to be extremely common in JavaScript. Let’s take a look at an example. We’ll use TypeScript to help make things a bit clearer.

const isFinite = (value: number | Number): boolean => {
  // don't use the global window.isFinite(value)
  // because it returns true for null/undefined
  return Number.isFinite(value);
};

...

export const NumberUtils = {
  isFinite,
  ...
};
Enter fullscreen mode Exit fullscreen mode
Finally, an appropriate place for a comment.

In this example, the comment is warranted, because it prevents future developers from introducing bugs without realizing it by changing return Number.isFinite(value); to just return isFinite(value);

Another common example is when you need to prevent bugs caused by Internet Explorer having different implementations of built-in functions than every other browser, because reasons. Let’s look at another example in TypeScript.

const addEntry<T>(set: Set<T>, value: T): Set<T> {
  // Don't directly return the value of `set.add`
  // because it's not chainable in IE 11
  set.add(value);
  return set;
};

...

export const SetUtils = {
  addEntry,
  ...
};
Enter fullscreen mode Exit fullscreen mode
Internet Explorer sucks.

Once again, in this example, the comment prevents a future developer from unknowingly introducing Internet Explorer-specific bugs by changing this implementation to just return set.add(value);


Modern programming languages have become so expressive that code comments are virtually obsolete, except in specific circumstances. The time has come to replace code comments with highly expressive, self-documenting code.

Top comments (7)

Collapse
 
slourenco profile image
SLourenco

Apart from the reasons you gave, I would argue that unit tests also remove the need for comments in several situations.

Aside from all the benefits that unit tests bring, they also can clearly define the intent of each code block. In your regex example, I would say that even without refactoring the method to simpler regex expressions, you can have multiple tests that help understand what that method is validating.

And unlike comments, unit tests can be enforced to be valid and updated when the code changes.

Collapse
 
matjones profile image
Mat Jones

That’s a very good point. The only caveat is that you have to actually open the separate file for the unit tests, which isn’t a big deal it’s just another level of separation between the code and it’s specifications.

Collapse
 
rossdrew profile image
Ross

You don't always need to. Many IDEs will run them in a window and display test names and failures. That can be explanatory enough in many cases

Collapse
 
rossdrew profile image
Ross

Oh my are there people who disagree :P

dev.to/rossdrew/should-comments-in...

Collapse
 
matjones profile image
Mat Jones

Of course code comments will be needed in some scenarios. But more often than not, they’re not needed, and about 95% of comments I see in practice are redundant, at least in my experience.

Collapse
 
rossdrew profile image
Ross

Completely agree. I think most of the time if you need to write a comment you've failed to write expressive code.

Collapse
 
ronnewcomb profile image
Ron Newcomb

Airight, the title drew me in.

I agree, but the % of comments which are needless depends a lot on the the employer.