DEV Community

Cover image for Code for People, Not for Machines - A Glimps into my Newest Book
Fernando Doglio
Fernando Doglio

Posted on

Code for People, Not for Machines - A Glimps into my Newest Book

I know this advice can sound counterintuitive, but when you write code you can’t think about the interpreter or the compiler or how it’ll translate into machine code. This isn’t your concern, instead, you should focus on who is going to read it tomorrow.

Even if you think no one else will read your code but you, the focus must still be the same. Coding sometimes requires us to come up with intricate solutions and trust me, they won’t always make sense the next day, even if you’re the one who wrote them.

Let’s pretend your new feature PoC works well and you and your team are now faced with the task of implementing its full version.

You’ll do two months of coding, and not only you, but three of your teammates will work on it, which means four developers writing code for the same feature. This causes people to interact with each other, and to write on each other’s files; this is completely normal and expected. Two days after starting to work on it, you find you and your team having daily meetings to review each other’s work to understand how your logic works.

As a result, you all spend a lot of time understanding code instead of writing it, and your Tech Lead asks all of you for an alternate solutions to your meetings. Something that you all can do that solves this problem now and for the rest of the project’s duration.

What would be your suggestion?

Self-documenting code doesn’t exist

One terrible solution some developers suggest to this problem is to write code that avoids complex logic solutions, using mnemotechnic variable and function names (i.e. instead of using foo and bar, they’ll use monthly_revenew and current_index). They say the code will be clean and easy to read, they say that the code itself can be the documentation. Mind you, this is indeed a good practice and something you should aim for, but by itself it’s not enough.

I’m afraid to say it but this is like asking Santa for a unicorn. It’s just not realistic, no matter how you look at it.

You’ll probably last a few days and then when deadlines hit or the weekend is close and you want to go home, user_with_too_many_logins becomes foo, and like that the whole self-documenting plan goes away.

Whatever solution you come up with needs to scale with code density and it needs to stay working over long periods of time. Saying you’ll document it eventually is not going to work because documenting two months’ worth of code is a massive task. A solution that takes more time than writing the code won’t work either. You must find a balance between the two.

Let me show you a few options and you pick the one that works best for you and your context. Whichever you choose to use, consider the following: it’s all about reducing the cognitive load of the person reading your code.

To put it another way, you need to remember that code is meant to be parsed into machine code and it has a particular structure, but people don’t parse and think the same as a machine parses code. We’re slower, and our memory is faulty, and when reading large chunks of code, we tend to get lost in the logic, and forget about where a piece of data is coming from. Whatever solution you choose to implement should help in that regard.

Commenting your code

This is the most obvious one, 99.99% of all programming languages support comments (I’m not going 100% because I’m sure there are a few esoteric languages out there that don’t bother with comments).

You can’t be satisfied with writing them, they need to make sense, be useful and stay updated.

Take a look at the Listing 1 and the great line of comment I left for you.

Listing 1: Sample log-in function for our fictional log-in form

    //log-in function
    async function performLogin(username, password, database) {
        let user = await database.query("select * from users where usr_name ='" + username + "' and password = '" + password + "'")
        if(!user) {
            return null
        }
        user.last_login = new Date()
        await user.save()
        return user
    }
Enter fullscreen mode Exit fullscreen mode

This could be a piece of logic for the new feature you’re working on. Granted, it’s not clean and it’s quite a simplistic approach but look at that useful comment at the top. Would you say that by reading it, you’re able to understand how to use the function? Or what it’s meant to do?
That line of comment literally serves no purpose, and the sad thing is I’ve seen comments like that countless times (heck, I’ve even written comments like that back in the day too).
For a comment to be useful and provide real help diminishing the cognitive load I mentioned before, it needs to:

  1. Add value to the reader. What is value? Anything the reader can’t know by looking at the function’s signature, or by looking at the line of code that the comment is meant to explain. If we’re dealing with a comment for a block of code, explain the internal logic and mention the steps involved in the process. Why not even add extra information you couldn’t possibly have otherwise, like types for your variables in JavaScript code? Are you commenting on a function or a method that returns something? Make sure you mention the returned value, what is it? What type is it? Anything and everything someone else trying to use your code might need must be explained, otherwise, it’s back to asking people or making mistakes.
  2. Be up-to-date. Reading an outdated comment and hoping it’ll help you figure out the code is as realistic as looking at the night sky and thinking the stars you’re seeing look like that right now. Any of them could’ve gone supernova already and you won’t find out in thousands of years. The same goes for the comments, if they’re not updated when the code is, then you’re reading an explanation for an older version which only helps to add confusion and bugs to the mix. If you’re updating code make sure you update the associated comments, and future you can thank you for it.

Some formatting standards can be used when writing comments that can be picked up by other tools, such as your IDE or documentation generation tools. These tools take the code and its comments, and mix them to better understand the code.

Look at figure 1 for a second.

Comparing the effects properly formatted comments have on your IDE
Figure 1: Comparing the effects properly formatted comments have on your IDE

Notice how the tooltips generated by the same code differ when a properly formatted comment is added into the mix. This is JavaScript code and the comments are following JSDocs standards. These standards tell you how to format your comments with a structure that a machine can then parse and get useful information out of. You clearly identify things such as variable types and names, return types, descriptions, and more, and then, in our case, the IDE parses it and show an informative tooltip.

You have the equivalent for other languages too, this isn’t only for JavaScript. Make sure to look for these standards for whatever language you use on your journey and apply them to your commenting practices. They won’t solve all your problems, but they’ll definitely help quite a lot.

You may not be right now, but eventually you’ll be part of a team of developers all working towards the same goal. The thing is, unless you’re all forced to, not everyone’s code will follow the same standards.

The language itself sets a syntax you need to follow and maybe the framework you use forces you to use one particular set of patterns in your code, but you still have a lot of wiggle room to add your own coding style. This isn’t a problem if everyone on the team has a similar style, but many aspects can change from developer to developer and unless someone sets a universal standard to follow, it can get messy.

A programming paradigm used by some frameworks (made popular by Ruby on Rails back in the day) is called “Convention over configuration” and it means you should expect to have “sensible default” behavior and only care about creating specific code for a section of your logic if that expected default is not met. For example, having a class representing a database table on your code, if that class is called “Notes” then you’d expect the table to have the same name and your class to have one property for each field on the table (again, aptly named and typed). Only if for some reason that expectation isn’t met, you’ll have to jump in and write the required code to map the values or whatever you need to make the class work between both worlds (this is your internal logic and the database structure).

Why am I suddenly talking about convention over configuration? Because you should expect your team and your coding standards to work the same way. Reading someone else’s code should feel like you’re reading your own. This helps alleviate some of the cognitive load associated with parsing code mentally to understand what it’s trying to do.

For example, having a set of standards that tell you:
● How to name your variables (for instance: always using English words and a camel-case notation or using specific prefixes to identify their type without having to look-up their definition). This by itself isn’t enough to document your code, but it’s a good starting point.
● How to perform specific minor tasks (such as comparing two values always with === instead of using == sometimes).
● The maximum length of a line of code (normally they force it to be eighty characters, which corresponds to the eighty characters terminals normally allow for).
● How to name your files or where to save them (such as having a specific folder where all common code should live).

I could go on, but you get the point. These standards are there to keep you from thinking about the meager tasks and worry about the important job of solving your problem. Mind you, these standards can be opinionated, after all, they’re likely coming from a single person (your tech lead). This is fine because you don’t have to worry about agreeing with them, but you must use them, like if you’re joining the team after it’s been working together for a while.

The good news is that there are tools known as “linters” that can help you check for these standards in your code. They’re normally included on your IDE or can be configured to run on pre-deployment or pre-merge events, making sure you’re not sending code which isn’t following the right conventions.

Coding standards are a great way to writing code which is easier to read by someone on your team, even if that someone comes years after you and you’re no longer working for the company. Granted, standards alone won’t make your code readable, but they are the equivalent of finding three different notes written in English, one from the year 1300, one from 1920, and one from last year. They’re all written in English but with slightly different styles; by translating them to the modern version of the language (i.e. forcing the same standard on all three of them), you (and everyone else from today) will have an easier time reading them.

A case for Literate Programming

Remember when I said earlier that there’s no such thing as self-documenting code? Well, hold my beer, because I’m about to go against my own words, kind of.

Literate programming is a programming paradigm that allows you to write code and documentation at the same time.

You see, what normally happens is that you first write your code and then write the documentation based on it. You could, potentially, go the other way around and with the documentation written use it as the development blueprints to create the code.

With Literate programming you write both at the same time. You create an explanation around your logic and show snippets of your code. This resulting documentation is processed and the working code is created and saved to one location, and at the same time a dynamic version of this documentation (i.e. normally an HTML version) is saved somewhere else.

To give you a quick example, pretend you’re building a JavaScript library meant to provide you with two functions: mult and div (multiplication and division). Following literate programming, you can write a document like shown in Listing 2.

Listing 2: The markup version of our documentation/source code

    ..    include:: <isoamsa.txt>                            
    ..    include:: <isopub.txt>

    Introduction
    -------------
    This is a sample documentation for a very simple JavaScript function

    MULT function
    --------------
    The MULT function requires two different parameters `a` and `b`, both numbers ideally.

    @o my-math-lib.js
    @{
    function mult(a, b) {
       return a * b
    }
    @}

    DIV function
    -------------
    The DIV function takes care of dividing `a` with `b` whenever possible.

    @o my-math-lib.js
    @{
    function subtract(a, b) {
        @<validate parameters@>
        return a / b
    }
    @}

    Value validation for the division
    ------------------------------
    Because one can't correctly divide a number by 0 (or a non-numeric or undefined value for that matter) using JavaScript, the following
    code takes care of checking for the value of `b`:

    @d validate...
    @{if(typeof b == "undefined" || isNaN(b) || b == 0) {
        return 0
    }@}
Enter fullscreen mode Exit fullscreen mode

As you can appreciate, there’s no one place where our code resides all together and organized; I’m splitting the code into three different chunks. Two of them are marked to be outputted to the same file (my-math-lib.js) and the third one is marked as a code snippet to include inside other sections.

The main benefit around this way of working is that you need to make a mental switch from only writing code (or only writing documentation) and need to think about both at the same time. The end result, when done properly, is a much richer documentation filled with examples, and the code is less prone to error because you’ve already thoroughly explained what it’s meant to do.

Originally introduced by Donald Knuth around 1984, this programming paradigm is great for small coding projects. Normally you’d want to create detailed documentation filled with examples for things like libraries and tools for other developers to use. These projects normally tend to be smaller than say, creating the internal intranet for the whole company.

The way it works is:

  1. You write everything together using a markup language (depending on the implementation, it can be Markdown, LeX, or something similar).
  2. You need specific custom tags to use across the text to signal elements such as code snippets, import statements (to split your writing into multiple files), and other elements that allow you to reuse blocks of text in different places.
  3. You’ll weave all the snippets together into the final source code.
  4. And finally, you’ll tangle the snippets and written documentation into an HTML file that acts as the user-facing documentation.

Don’t worry too much about the weaving and tangling phases (the last two steps), because they’ll be taken care of by pyWeb, the tool we’ll use to parse the code from Listing 2.

As a result of using it, we get two files: my-math-lib.js which as you can see in Listing 3, it’s full-on JavaScript without any extra documentation, and my-math-lib.html which is the dynamic documentation.

Listing 3: Final version of our code, without any extra documentation

    function mult(a, b) {
        return a * b
    }

    function subtract(a, b) {
        if(typeof b == "undefined" || isNaN(b) || b == 0) {
          return 0
        }
        return a / b
    }
Enter fullscreen mode Exit fullscreen mode

Figure 2 shows the tangled version of the same file, this time rendered as interactive HTML.

The tangled version of the documentation
Figure 2: The tangled version of the documentation, mixing code and comments

The HTML is pre-formatted by default, but you can edit it and its stylesheet to make it look any way you like. The point here is that the hardest part, which is writing the HTML to publish somewhere is already done for you. Changing styles to make it look better is a minor hustle in comparison.

Is this the answer to all our documentation problems? Does this programming paradigm change the way we work and write our code? I don’t think it does because larger projects still require a focus on the code and folder structure that Literate programming can’t provide. Smaller ones though, such as libraries and small frameworks, can take advantage of it without seeing their coding workflow changed that much. It’s definitely an interesting alternative compatible with all programming languages, and a great option if you’re looking to save some time writing documentation.

Readable code >> one-liners

Although documentation in all its forms is crucial to helping people understand how code works, there’s something to be said about the way you write your code. Sometimes a single line of code might require several paragraphs of documentation because it’s too optimized and minified that mentally parsing it takes too long. This is plain unacceptable in most situations.

Unless you’re writing embedded code for resource-constrained devices, saving characters and writing doesn’t yield huge benefits. Consider, instead, writing the same solution for human beings and machines alike (i.e. code that works, but which is a lot easier to read). Check out Listing 4 for an example of what I mean by that.

Listing 4: Readable vs optimized code

    salaries = {
      "You": 100000,
      "Sarah Connor": 90000,
      "John Doe": 100000,
      "Laura Micheals": 60000,
      "Mike Tyson": 35000
    }

    def who_makes_the_most():                                                            # A
      dict_sal = salaries.copy()
      high_sal = 0
      for name in dict_sal:
        dict_sal[name] = dict_sal[name]
        if dict_sal[name] > high_sal:
          high_sal = dict_sal[name]

      high_paid_emp = []
      for name in dict_sal:
        if dict_sal[name] == high_sal:
          high_paid_emp.append(name)

      return high_paid_emp


    def who_makes_the_most_v2():                                                         # B
      return [emp for emp in salaries if salaries[emp] == max([sal for sal in salaries.values()])]

    print who_makes_the_most()
    print who_makes_the_most_v2()

    #A - Version 1, calculate who makes the most money at the company. Verbose and understandable version.
    #B - Version 2, calculate who makes the most money with a reduced and harder-to-read function.
Enter fullscreen mode Exit fullscreen mode

Both versions of the same function in Listing 4 yield the same result, but one requires a single line of code yet the other eleven. Is V2 better than the original? This is a subjective question to answer. A team of expert Python developers with many years of experience doesn’t need a lot of time to read the one-liner function, but the moment someone new comes in with the intention of editing or expanding on it, that person will face some issues. This isn’t only because the function is a lot harder to understand, but the fact that it’s been reduced to a single line with the same amount of functionality means the complexity has been packed into specific constructs.

In the case of this example, there are two list-comprehension expressions nested and a call to the max function. If we want to keep adding logic into our code, for example, upper casing the names and removing the first names from the returned values, we need to expand our one-liner function, making it harder to read and adding an in-line check for scenarios with no last names. Listing 5 shows how the new functions grow based on this added complexity:

Listing 5: Example from Listing 4 expanded with added logic

    def who_makes_the_most():                     # A 
      dict_sal = salaries.copy()
      high_sal = 0
      for name in dict_sal:
        dict_sal[name] = dict_sal[name]
        if dict_sal[name] > high_sal:
          high_sal = dict_sal[name]

      high_paid_emp = []
      for name in dict_sal:
        if dict_sal[name] == high_sal:
          name_parts = name.upper().split(' ')
          if len(name_parts) == 2:
            high_paid_emp.append(name_parts[1])
          else:
            high_paid_emp.append(name_parts[0])

      return high_paid_emp


    def who_makes_the_most_v2():   # B
      return [emp.split(' ')[-1].upper() for emp in salaries if salaries[emp] == max([sal for sal in salaries.values()])]

    #A - Version 1 with extra logic added into it
    #B -  Version 2 of the same function turned into a longer line
Enter fullscreen mode Exit fullscreen mode

You can see how the code from the original function remains a lot easier to mentally parse. You’ve a clear place where you can check whether there are last names to use and what to do if there aren’t. With the one-liner, we resorted to using index -1, which if you don’t know what it means, it references the first element starting from the end of the list (the last one). This works, but using negative indexes on arrays is something not everyone is comfortable with and requires you to understand how Python specifically deals with them.

Again, both functions do the same, but with this basic example, you can see how we still manually translate the packed logic instead of letting you read it.

Granted the longer function could also be simplified and still maintain a better readability than V2, but the point here’s to show you both extremes. You shouldn’t aim for either, you should aim for something in-between, something like Listing 6 below.

Listing 6: A new version, capturing the best of both worlds

    def who_makes_the_most_v3():
      dict_sal = salaries.copy()
      high_sal = max(dict_sal.values())

      high_paid_emp = []
      for name in dict_sal:
        if dict_sal[name] == high_sal:
          name_parts = name.upper().split(' ')
          if len(name_parts) == 2:
            high_paid_emp.append(name_parts[1])
          else:
            high_paid_emp.append(name_parts[0])

      return high_paid_emp
Enter fullscreen mode Exit fullscreen mode

We get rid of the first for loop, because there’s already a function that does this for us: max. We’ve kept the “complex” section of our logic intact and expanded because it’s a lot easier to read that way.

With all of this being said, you need to remember one thing: readable code doesn’t substitute documentation, they complement each other. You still need to document your algorithm and explain why you’re doing what you’re doing. Writing readable code can help you avoid documenting some sections in detail, which saves you a bit of time.

Understandable code is no excuse to overcomplicate a solution. Consider this as you try to make your code easy to read--you can also fall for a common trap: overengineering.

That’s all for this article. If you want to learn more about the book, check it out on Manning’s liveBook platform here.


From Skills of a Successful Software Engineer by me
When writing code, you need to write it with people (rather than machines) in mind. Without good notes and documentation, others who read your code might have to spend a lot of time trying to understand it before they can begin to work with it.

Take 35% off Skills of a Successful Software Engineer by entering fccdoglio into the discount code box at checkout at manning.com.

Top comments (0)