DEV Community

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

Posted on

6 1 1 1 1

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 getStudentRecordById(record){
       return recordId == record.id; 
     }
     return studentRecords.find(getStudentRecordbyId);
  }
  function alphabetizeRecordsByName(a,b){
    return a.name > b.name ? 1: -1; 
  }
  function printEachRecord(record){
    console.log(
      `${record.name} (${record.id}): ${record.paid ? 'Paid': 'Not Paid'}`
     );
  }
  recordIds.map(findStudentRecords)
  .sort(alphabetizeRecordsByName)
  .forEach(printEachRecord); 
}

//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?

** Image by Tumisu from Pixabay

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

Top comments (9)

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
 
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
 
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
 
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
 
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
 
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.

The Most Contextual AI Development Assistant

Pieces.app image

Our centralized storage agent works on-device, unifying various developer tools to proactively capture and enrich useful materials, streamline collaboration, and solve complex problems through a contextual understanding of your unique workflow.

👥 Ideal for solo developers, teams, and cross-company projects

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay