Better Writers Make Better Programmers

Chase Grainger
12 min readMay 22, 2021
Photo by Cathryn Lavery on Unsplash

The best code is most often produced from an elite group of individuals: from good programmers who are also good writers.

Humans have short attention spans. To keep the reader’s focus, the writer must follow a few rules.

Many of the principles skilled writers follow during the writing process directly correspond to the fundamentals skilled programmers embody during the software development process.

Straight to the Point

A skilled writer knows that each word matters. Word placement, sentence organization, paragraph flow — these all contribute towards an effective presentation of the essay’s main focus. Likewise, every block of code should have a purpose. Length of code should be intentional. Every variable, every expression, every function and every program should be crafted in the most efficient manner to accomplish the program’s goal.

Composition concision can be achieved by:

  • Removing unnecessary fillers & redundant words and phrases
  • Preferring straightforward words over fancy words
  • Choosing to write clearly

Unnecessary Fillers & Redundancy

In writing, fillers are words or phrases that can be removed and still maintain the sentence’s purpose. Using fillers, redundant phrases or inapplicable words shows an unclear vision from the writer — resulting in an unclear understanding for the reader. The writer’s goal must be to minimize the distance between the writing’s main focus to the user’s current understanding. This remains true in programming — correlating implementation to other programmers’ understanding of the implementation.

In programming, fillers are expressions or groups of expressions that can be removed or shortened and still maintaining the system’s purpose (system can refer to function, class, etc.).

Consider the function below, whose purpose is to find the largest of two numbers:

function max(num1: number, num2: number): number {
const firstNum = num1;
const secondNum = num2;
let largestNum;
if (firstNum > secondNum) {
largestNum = firstNum;
} else {
largestNum = secondNum;
}
return largestNum;
}

max contains unnecessary fillers: firstNum, secondNum and largestNum; each of these variables can be removed and replaced with simpler logic that maintains the purpose of the max function.

firstNum, secondNum and largestNum are also redundant:

  • Both firstNum and secondNum redefine values already provided from the function arguments.
  • largestNum is assigned a value in two different locations, indicating unnecessary redefinition.

By removing fillers and eliminating redundancy, max can be improved to:

function max(num1: number, num2: number): number {
return num1 > num2 ? num1 : num2;
}

The result is one, simple expression — still serving its initial purpose.

Choosing Straightforward over Fancy

It’s more difficult to understand the following:

The family is bereft of water.

Than it is to understand the latter:

The family has no water.

Straightforward words depict the author’s intentions more clearly than fancy words. Straightforward words offer less room for guessing. Being straightforward can mean an increase or decrease in length — as long as readability is either maintained or increased.

This remains true in programming logic. Implementation should be straightforward so other programmers can understand it. Though some fancy algorithms provide optimization for larger time complexities, they slow the programmer’s understanding of the implementation. Keep it simple, stupid (KISS).

“Fancy algorithms are slow when n is small, and n is usually small. Fancy algorithms have big constants. Until you know that n is frequently going to be big, don’t get fancy.“ — Ken Thompson

It’s from a solid understanding of implementation where more useful programming constructs can be created — take array iteration methods in JavaScript, or the Rule of Least Power.

Choosing Clarity

It’s easy to get a point across to the reader vaguely, just as it’s easier to dump all the recyclables into a trash can. It’s also easier to get a function working with an incredibly difficult-to-read implementation. Just because it’s easy does not make it the right thing to do.

It’s more difficult to get a point across to the reader clearly (and purposefully). Standard convention pushes writers to:

  • Replace vague words (verbs especially) with specific words
  • Limit uses of forms of the word “be“
  • Avoid clichés

Programmers must also be held to a high standard with regards to naming conventions.

Choosing proper names for variables can be difficult, but it makes all the difference for when another (or the same in the future) programmer contributes to an existing codebase. Clearer construct names result in clearer implementation.

putTogether is an example of a poorly named function containing poorly named function arguments:

function putTogether(a: number, b: number): number {
return a + b;
}

The purpose of putTogether is obvious only after reading its implementation. It’s better to have function and argument names reflect their purpose. A function’s objective should be derivable from its signature alone — putTogether could mean any mathematical operation.

By choosing clarity, putTogether can be named to reflect its intended purpose (derived from its implementation), putting all invalid assumptions of the programmer aside:

function add(num1: number, num2: number): number {
return num1 + num2;
}

Separating Longer Sentences

Long, complex sentences should be avoided in writing. Sentences forcing the reader to remember multiple ideas can hinder their understanding of the essay as a whole. Sentences should be clear and concise with a single point.

A common tactic is to separate longer sentences into one of the following:

  • Shorter sentences
  • Lists

By separating longer sentences, it’s easier for the reader to understand the point the author is trying to get across. The writer is choosing to combine words and shorter sentences to most effectively represent the essay’s purpose. The writer is choosing to create well-defined written units.

A unit is an individual component of a complex whole, working together with other units for a unified purpose. In programming, units can include:

  • Classes
  • Functions
  • Expressions
  • Variables

Unix programming focuses heavily on systems comprised of concise units. The author, Eric Raymond, explains the Rule of Modularity and how simple parts (units) ought to have clear and concise purposes connected by clean interfaces.

Take this long, complicated expression. The totalPrice variable expression calculates the cost of purchased items, along with a shipping fee. The shipping fee is free for orders over $50, or if the customer decides to pick up the items in store; otherwise it is calculated based on the customer’s address.

const totalPrice = itemOne.price + itemTwo.price > 50 || pickupMethod === PickupMethod.InStore ? itemOne.price + itemTwo.price : itemOne.price + itemTwo.price + 3.55; // in dollars

This expression contains many calculations. Because all the logic is condensed to a single line, it’s harder to debug if errors were to occur. totalPrice could be much more simpler to understand.

By using the writing technique of separating out longer constructs into shorter constructs, we can separate totalPrice into shorter expressions as clearly-defined constants.

The purpose of totalPrice is more clearly represented:

const itemPrices = itemOne.price + itemTwo.price;
const threshold = 50; // dollars
const freeShipping = itemPrices > threshold || pickupMethod === PickupMethod.InStore;
const shippingCostToCustomer = 3.55; // dollars
const shippingFee = freeShipping ? 0 : shippingCostToCustomer;
const totalPrice = itemPrices + shippingFee; // $59.49

It’s easier to reason with multiple, simple expressions than it is with a single, complex expression with embedded calculations. When bugs occur, the point of error now becomes narrower and clearer.

As previously mentioned, an increase in length does not always signify a decrease in readability. In this case, refactoring the totalPrice expression made it much simpler to understand. Though 1 line of code has been lengthened to 4, the meaning is clearer. An increase in code length is acceptable if clarity also increases. If logic is represented clearly, it makes all the difference for how other programmers will view it in the future.

You can find a full code example here.

Using Jargon Appropriately

Merriam-Webster defines jargon as “the special terms or expressions of a particular group or field.“

Within specialized fields, jargon is required to communicate clearly and precisely. For those outside the field, it’s confusing and intimidating. In order to use jargon appropriately, a writer must understand the intended audience. A generalized audience with no guaranteed experience in the field of a writer’s research paper would benefit very little from a high use of jargon. An audience of researchers in the same field, however, increases the need for jargon. Other researchers would understand the jargon the writer would use. Jargon includes precise and complex terms scholars rely on to communicate with each other most effectively.

If jargon is not necessary to communicate a point clearly, the writer need-not use it.

If using jargon is the most accurate way to get a point across, the writer must force the reader to learn the term being used even if the composition increases in length. It is sometimes necessary to increase or decrease the length of an essay through jargon use to increase accuracy of the author’s intentions.

The same applies to code logic: implementation precision must not be sacrificed to prevent other programmers from having to learn to understand what the code achieves. There is a difference between unnecessary complexity and complexity restricted to a certain level of precision, available from programming constructs that are presently available (see this link or Choosing Straightforward over Fancy above).

Consider the following:

function doubleNumbers(numbers: number[]): number[] {
return numbers.map(num => num << 1);
}
const numbers = [1, 2, 3];
const doubledNumbers = doubleNumbers(numbers); // [2, 4, 6 ]

doubleNumbers creates an unnecessary abstraction of the map array method. Within map, the programmer also uses the right shift operator. Though the right shift operator’s primary purpose is to shift a specified number of bits to the right, here the programmer uses it to double each number in numbers. The final product does achieve doubleNumber‘s objective, but the programmer’s intent of what <<what it accomplishes do not match.

In this example, there are two unhelpful forms of jargon:

  • doubleNumbers is jargon introduced by the programmer to create an unnecessary abstraction of map
  • The right shift operator << is jargon used by the programmer that will be unfamiliar to most programmers and represents implementation that doesn’t match the programmer’s intention

Though the purpose of doubleNumbers is somewhat clear, it creates another layer other programmers must traverse through to find that all doubleNumbers uses is the map array method.

By creating a doubleNumbers function to contain map, the programmer has created an abstraction of map. Code length has been increased without increasing the ease to learn how the program doubles numbers; code length should only increase or decrease if readability increases.

There is one example of helpful jargon:

  • map is an abstraction used to represent a common application of loops

This is an example of where jargon should be used. It’s much more precise to use map in the same expression, rather than creating another function to perform the same logic. When necessary, programmers should be okay with expecting other programmers to become familiar with useful, relevant constructs, like map. Forcing other programmers to learn how to use a right shift operator << where its proper usage doesn’t match the intention of the programmer is uncalled-for.

The deciding factor of whether or not jargon is necessary depends on the target audience and the type of jargon being used. If an expression can be accurately understood as it is without introducing or using unnecessary jargon, use the expression as it is; if it cannot, introduce or use the essential jargon.

By eliminating unnecessary and unhelpful jargon, map can be doubledNumbers as:

const numbers = [1, 2, 3];
const doubledNumbers = numbers.map(num => num * 2); // [2, 4, 6]

Redefining the Presentation

An essay’s first version is never the final version. This is why initial composition versions are denoted as “rough drafts“ — the first draft’s quality never compares to the final draft’s quality. In both essays and programs, written content goes through many revision processes. This is to ensure the composition’s main purpose is expressed to other readers in the most accurate and efficient manner possible.

In writing, it’s mind-boggling to think a professional writer would publish an article’s first draft without any revision. A good writer knows that getting the point across to the reader is not enough — the presentation of the article must be revised in order for the reader to learn most effectively. The writing is never finished after the main points are acknowledged. The final submission is always thoughtfully revised — this should happen with code as well.

In code, having a seemingly-working initial implementation is not enough. Yes, the functionality may exist — the system’s purpose is supposedly achieved. However, there’s a caveat — the code is most likely incredibly lengthy and difficult to understand. There are unintended consequences the original programmer had not prepared for. It will be challenging to understand for any programmer wanting to contribute to the codebase in the future (not to mention the same programmer).

The entire composition, whether in writing or in programming, must be thoughtfully revised — beginning at the smallest increments. It’s at these small improvements that crucial, sometimes intensive refactoring steps take place. A writer may change the structure of a sentence, leading to the reworking of a paragraph, concluding with a revamped composition entirely. A programmer may see that after changing the name of variable to represent its purpose more clearly, that the expression represented by the variable would serve better as a function. After creating this function, the programmer may find that other functions can be created to serve similar purposes and be combined into a class with a uniform objective.

Revision takes time. However, the time sacrificed tends to produce much clearer and easier to read content for the user. Clearer code is always return on investment. Redefining the presentation of materials is crucial for the quality of any composition.

The Power of a Thesis

If the writer’s vision isn’t clear, neither is the reader’s.

Knowing what makes a good thesis statement can benefit programmers by:

  • Increasing his ability to code intentionally
  • Increasing quality of tests

Coding Intentionally

A thesis is a statement of purpose, one to two sentences, summarizing the entire focus of an essay.

Skilled writers know that each sentence paragraph must point back to the essay’s main focus, and each sentence in a paragraph must point to the argument its paragraph is making. Applicable knowledge of a good thesis enables the writer to stay on track with the essay’s focus throughout the composition process.

Without a thesis statement, it’s hard for the writer to identify the essay’s purpose — making it impossible for the reader.

Programmers must also maintain a program’s focus throughout the coding process. This allows for intentionality in each step towards the program’s main objective. By keeping the program’s objective in mind during development, variable names are concise. Function signatures accurately depict their behavior. Classes indicate carefully separated functionality. Each unit better reflects the system’s unified purpose.

A constant, clear focus of a system’s goal also enables the file hierarchy of a system to be better organized. Having a visible conceptual layout often results in a better defined hierarchical layout.

Increasing Quality of Tests

Tests either pass or pail — there is no in between. This means that tests differ in quality. By translating knowledge of a thesis to programming and having a clear idea of a software system’s focus, test quality improves. What to test becomes more evident and programming time is not wasted.

In acceptance testing, tests should indicate correct system behavior. In unit testing, tests should point directly to units that contribute to the system’s correct behavior.

Consider a simple translator application. Imagine it looks something like the following:

The user types the phrase he wants to translate in the left-most box. To begin translation, he clicks the blue Translate button; the button turns from blue to green after translation is complete. The translated text then shows in the right-most box.

Without knowledge of what makes a good thesis, a programmer may write tests that look like this:

Bad_Test {
button.color = blue
translateButton.click()
expect(button.color).toBe(green)
}

Bad_Test verifies the UI performs the appropriate styling. This is great — however, this test does not verify the system’s objective, which is to translate text from one language to another. This programmer is not applying knowledge of a thesis to his code.

Bad_Test should be rewritten with the system’s focus in mind, as a thesis enforces. A more appropriate test would be to check if the system successfully translates text from one language to another:

Good_Test {
message = "hello"
translatedMessage = translate(message).fromEnglish().toSpanish()
expect(translatedMessage).toBe("hola")
}

Good_Test is relevant to the system and successfully verifies that the translator application is working as expected. Just as each sentence in an essay should point back to the thesis statement, tests should point back to the system’s overall purpose.

Conclusion

The benefits of programmers understanding and applying standard convention writing techniques are invaluable. Effective programmers focus on not only achieving a system’s goal, but by achieving its goal in the clearest, most readable manner.

By choosing to write clear-cut code using the techniques of writing, software inevitably reaches its highest possible level of quality — as it should.

--

--