DEV Community

Cover image for Writing Human Readable Code: To Name or Not To Name
Bola Adebesin
Bola Adebesin

Posted on • Edited on

Writing Human Readable Code: To Name or Not To Name

I waffled back and forth about the title of this post, but in the end, I couldn't resist the Hamlet reference.

Today's topic is about writing clear, human-readable code. I care about this for a few reasons. When code is easier to read/understand, it's easier to:

  • Maintain
  • Refactor
  • Discuss (with clients, colleagues, your rubber duck etc).

There are so many instances where I've come across old code that I don't understand.

Star Wars Meme about difficult to read code

This frustrating experience has ignited my interest in learning how to craft "better" code.

The good news is, there are a bunch of blog posts, forums, books, and articles about how to write better code. A few tips I've seen:

  • Add comments to your code; Comments that explain "why" are better than comments that explain "what"
  • Write tests
  • Name your variables clearly
  • The single responsibility principle: every function should do one job really well
  • Avoid complicated conditionals
  • Keep files to 250 lines of code or less

I coud go on, but there is one piece of advice that really struck me and I want to discuss it. The advice was: "Use traditional function declarations and opt for named functions over anonymous ones."

This advice caught my attention because my code is littered with anonymous functions. I can't count how many unnamed functions I've passed into .map, .forEach, and .reduce methods.

I don't think I'm alone in this. I've read time and time again that arrow functions and anonymous functions help to make code more concise and that this brevity makes for smaller files, which is part of writing better code, right? Well, it turns out that concise and anonymous does not necessarily mean better or more readable.

The more I think about this, the more it makes sense. "Well-named variables" is one of the bullet items for achieving clearer code. Variables with clear names help us to understand the data contained within them. This should also be true for functions which not only contain data, but often manipulate that data in some way too.

Let's look at a small example pulled from the Frontendmasters course, "Deep JavaScript Foundations V3" by Kyle Simpson, author of the "You don't know JS" series.

I am going to share two different implementations I wrote for a function called printRecords. One uses named functions and the other does not.

The function, printRecords should:

  • Accept an array of numbers (these numbers represent student Ids)
  • Retrieve each student record using the student Id
  • Sort these records alphabetically by the students' names
  • Print each record to the console in the following format: Name (student Id): Paid (or Not Paid)
var currentEnrollment = [410, 105, 664, 375] 
var studentRecords = [
    { id: 313, name: "Frank", paid: true, },
    { id: 410, name: "Suzy", paid: true, },
    { id: 709, name: "Brian", paid: false, },
    { id: 105, name: "Henry", paid: false, },
    { id: 502, name: "Mary", paid: true, },
    { id: 664, name: "Bob", paid: false, },
    { id: 250, name: "Peter", paid: true, },
    { id: 375, name: "Sarah", paid: true, },
    { id: 867, name: "Greg", paid: false, },
];

// Named Functions Version: 
function printRecords(recordIds){
  function findStudentRecords(recordId){
     function hasStudentId(record){
       return recordId == record.id; 
     }
     return studentRecords.find(hasStudentId);
  }
  function alphabetizeRecordsByName(a,b){
    return a.name > b.name ? 1: -1; 
  }
  function logRecord(record){
    console.log(
      `${record.name} (${record.id}): ${record.paid ? 'Paid': 'Not Paid'}`
     );
  }
  recordIds.map(findStudentRecords)
  .sort(alphabetizeRecordsByName)
  .forEach(logRecord); 
}

//Anonymous Functions Version: 

function printRecords(recordIds){
    recordIds
        .map((recordId) => 
            studentRecords.find((studentRecord) => studentRecord.id == recordId)
        )
        .sort((a,b) => a.name > b.name ? 1: -1)
        .forEach((student) => {
            console.log(
                `${student.name} (${student.id}): ${student.paid ? 'Paid': 'Not Paid'}`
            )
        })
}


printRecords(currentEnrollment); 


// Console: 
/*
 Bob (664): Not Paid
 Henry (105): Not Paid
 Sarah (375): Paid
 Suzy (410): Paid
*/

Enter fullscreen mode Exit fullscreen mode

Looking at the two implementaions side-by-side in my editor, I can see that the named version of printRecords spans lines 1-23, while the anonymous version spans lines 1-12. Some of this is a result of the automated code formatter I use, but right away it's clear that the anonymous version is more concise.

Still, when I look at the named version, I understand what each function in the .map, .sort, and .forEach method is doing. I don't have to read the code line by line. It's all right there in the names: find the student records, sort them alphabetically by name, print each record. By contrast, the anonymous version requires me to look at the code in the body of each function to understand what is happening.

In this case, I would rather take the extra 11 lines of code for a better experience modifying this code down the road.

In his lecture, Kyle Simpson posits that there are at least three reasons to choose named functions over anonymous ones:

  1. Reliable self-reference (e.g. If a function has a name, it can do things like call itself for a recursive solution)

  2. More debuggable Stack Trace (if the function has a name, the stack trace will provide it)

  3. More self documenting code

What do you think? Would you give up anonymous functions if it meant more human readable code? Do you use the number of lines of code as an indicator of whether a piece of code needs to be rewritten? How do ensure your code is readable 2 months down the road and beyond?

Update March 13, 2025:

I received some really great feedback on this post that I wanted to acknowledge because it encouraged me to think more (a.k.a I went down a bunch of rabbit holes) about naming conventions and not just clarity of code, but also the clarity that inherently exists (or doesn't) in human language itself.

Thank you:
@moopet
@devpacoluna
@pengeszikra
@lionelrowe
@decaf_dev
@ddfridley

The comments you left are great jump off points for me and others to continue learning and growing should they stumble across this post in the future!

** Image by Tumisu from Pixabay

Top comments (17)

Collapse
 
devpacoluna profile image
Paco Luna

As a matter of fact, you can use named functions to create a very useful library to your project. Just use anonymous functions that you certainly know that are very simple or one in a million in your project.

Collapse
 
mobolanleadebesin profile image
Bola Adebesin

@devpacoluna Thanks for your comment! I hadn't thought of using named functions for building a library, is that something you've done?

Collapse
 
devpacoluna profile image
Paco Luna

yep, I work with react. So i create a folder lib/ and each file has some functions with the same purpose example date.ts, car.ts, etc...

Collapse
 
pengeszikra profile image
Peter Vivo • Edited
// named arrow version

const findStudent = (recordId) => studentRecords
  .find((studentRecord) => studentRecord.id == recordId);

const nameSorting = ({name: aName},{name: bName}) => aName > bName 
    ? 1
    : -1;

const consoleRender = (student) => console.log(
    `${student.name} (${student.id}): ${student.paid ? 'Paid': 'Not Paid'}`
);

const printRecords = (recordIds) => recordIds
        .map(findStudent)
        .sort(nameSorting)
        .forEach(consoleRender)
;
Enter fullscreen mode Exit fullscreen mode
Collapse
 
mobolanleadebesin profile image
Bola Adebesin

Thanks for sharing this. I like that your version combines arrow functions and semantic naming

Collapse
 
artamonovkirill profile image
Kirill Artamonov

There are only two hard things in Computer Science: cache invalidation and naming things.

-- Phil Karlton

Named functions are great when they describe common concepts. E.g., I'd have quite some confidence in how sum looks, even if it were declared in a different file:

const numbers = [1, 2, 3]
const sum = (acc, n) => acc + n
numbers.reduce(sum)
Enter fullscreen mode Exit fullscreen mode

But for functions with different possible implementations (especially the logRecord, where log structure shapes the feature's "look and feel"), I'd start with anonymous functions in order to understand the entire logic as I read the code line by line—without the need to scan the file (or even jump between files) for the declaration of a specific function. If the context of a specific piece of code gets too big, I'd look for ways to extract coherent parts. I find it important to let the code and your understanding of it evolve for a while, rather than zealously extract every anonymous function.

Collapse
 
mobolanleadebesin profile image
Bola Adebesin

Thanks for sharing this perspective. That approach makes sense to me and I’ve heard a variation of that quote before but didn’t know the source!

Collapse
 
artamonovkirill profile image
Kirill Artamonov

Here are some more variations: martinfowler.com/bliki/TwoHardThin... 🤗

Thread Thread
 
mobolanleadebesin profile image
Bola Adebesin

😂 love it!

Collapse
 
moopet profile image
Ben Sinclair

I like this and agree mostly, but I think the examples you've given add to the confusion rather than making it clearer:

Reading the following, I'd be confused expecting getStudentRecordById to take an ID as a parameter and to return a record object, when in fact it takes a record object and returns a boolean. That's suitable for use in the find method, but it doesn't make sense in itself:

  function findStudentRecords(recordId){
     function getStudentRecordById(record){
       return recordId == record.id; 
     }
     return studentRecords.find(getStudentRecordbyId);
  }
Enter fullscreen mode Exit fullscreen mode

If I was reading this one, printEachRecord would imply to me that it was a function which took some kind of iterable, rather than a single record:

  function printEachRecord(record){
    console.log(
      `${record.name} (${record.id}): ${record.paid ? 'Paid': 'Not Paid'}`
     );
  }
Enter fullscreen mode Exit fullscreen mode
Collapse
 
mobolanleadebesin profile image
Bola Adebesin

@moopet thanks for your comment. You raise a good point and one that someone in another comment brought up.

Named functions can only provide additional clarity if they are named well. And there is definitely room for improvement for how I named these functions.

Collapse
 
lionelrowe profile image
lionel-rowe

Given the simplicity of the functions in your example, I think the anonymous/inline version is significantly more readable. That's partly due to the increased conciseness, which cuts out unnecessary noise, but mostly to do with the reduced indirection compared to the named version. Unnecessary indirection is really bad for readability, because it forces you to jump around instead of reading linearly.

If each step contained more logic, named would probably be better (indirection can be a good thing when dealing with significant chunks of abstraction), but in that case most functions should be defined at the top level rather than nested. That way they can be tested individually.

The exception is where the function depends on variables in the outer scope, which are sometimes better defined nested within that scope. Other options would be refactoring to take all their dependencies as explicit arguments, or using a class with methods and internal state.

Collapse
 
mobolanleadebesin profile image
Bola Adebesin • Edited

@lionelrowe thanks for your comment. I agree the logic is simple enough that it might not be the best example.

My thought was that readers would see the potential in this style of code for larger, more complex code bases, even if the examples were simple.

Although, as others have pointed out, this approach requires the names of the functions to be clear, which I could have done a better job on too.

I appreciate the feedback and perspective!

Collapse
 
decaf_dev profile image
decaf Dev

I personally like to take the approach of bringing the helper functions out of the important business logic while using something like ramda to make that magic possible:

import { curry } from "ramda";

/**
 * @param {{id: number, name: string, paid: boolean}[]} records 
 * @param {number[]} ids 
 */
export function printRecords(records, ids) {
  const byRecordIsInListOfIds = curry(recordIsInListOfIds)(ids);
  records.filter(byRecordIsInListOfIds)
    .sort(compareRecordNames)
    .forEach(printRecord);
}

/** 
 * @param {number[]} ids
 * @param {{id: number}} record
 */
function recordIsInListOfIds(ids, record) {
  return ids.includes(record.id);
}

/**
 * @param {{name: string}} a 
 * @param {{name: string}} b 
 */
function compareRecordNames(a, b) {
  return a.name > b.name ? 1 : -1;
}

/**
 * @param {{id: number, name: string, paid: boolean}} record 
 */
function printRecord(record) {
  console.log(
    `${record.name} (${record.id}): ${record.paid ? 'Paid' : 'Not Paid'}`,
  );
}
Enter fullscreen mode Exit fullscreen mode

Here the curry method will allow us to do some fancy functional programming and not have to resort to something like recordIsInListOfIds.bind(null, ids).

Collapse
 
mobolanleadebesin profile image
Bola Adebesin

@decaf_dev thanks for sharing this method, I’ll have to check out Ramda!

Collapse
 
ddfridley profile image
David

As a software engineer with decades of experience I found the anonymous function version easier to read. I also point out that English has a problems with being imprecise and having different meanings for different people in different contexts. I do appreciate good variable names and comments about why, but somehow shorter is quicker and easier to digest.

Collapse
 
mobolanleadebesin profile image
Bola Adebesin

@ddfridley Thanks for your comment! You're right, English can be imprecise, and I hadn't considered the confusion that might arise from that. Maybe it's be worth it to let the code, speak for itself.