Skip to content
academia.sh

Lesson 01 / 16

Meaningful Names

Treating naming as a measurable design decision: counting opaque arguments at the call site in a shortened and an intent-revealing version of the same shipment fee calculation, deriving how many candidate words an abbreviation could resolve to, and the relationship between name length and the scope a name lives in.

Contents

Completing a language curriculum yields one capability: writing code that produces the requested behavior. The JavaScript and TypeScript courses delivered that; if the tests pass, the program is correct. This curriculum opens with a different question. When a choice must be made between two programs that produce the same behavior, what does the choice rest on?

The question is unavoidable, because code is not written once and abandoned. A shipment fee library gets a new zone coefficient six months later, and a volumetric weight divisor changes a year after that. Whoever makes those changes reads the code first. Reading cost is the actual measure of a design, and a claim like “more readable” does not settle an argument until tied to a number. This lesson ties naming to two numbers: the number of arguments at the call site whose meaning cannot be answered, and how many different domain words an abbreviation could resolve to.

One library runs through the course: shipment fee calculation and routing. Its first function computes a fee from a shipment’s weight, volume, and zone.

Measure: The Question Answerable at the Call Site

The information a name carries is measured where it is used. Someone looking at a call line asks: which parameter does this argument fill, what does its value mean, what is the result a fee for? If answering requires opening another file, the name is not carrying information.

To make this mechanical, call an argument labeled if the call text itself states which parameter it fills — a named field, or a variable name of at least three letters. Otherwise it is opaque: a bare number, a boolean, or a single-letter name. The number of opaque arguments is a lower bound on the number of questions that cannot be answered at the call site.

The Abbreviated Version

The first version works and passes its tests. It computes the volumetric weight, finds the billable weight, selects the tier, applies the zone coefficient, subtracts the discount, and does not drop below the minimum fee.

mkdir -p version-a version-b
// version-a/data.mjs — sample shipment and tariff
export const s = { w: 2.4, wd: 30, l: 24, h: 18 };
export const t = { tr: [{ u: 1, r: 38 }, { u: 5, r: 26 }, { u: 10, r: 21 }, { u: Infinity, r: 17 }],
                   zc: { 1: 1, 2: 1.35, 3: 1.8 }, mf: 45 };
// version-a/fee.mjs — shipment fee, abbreviated names
export function calc(s, t, z, x) {
  const v = (s.wd * s.l * s.h) / 3000;
  const a = Math.max(s.w, v);
  const k = t.tr.find((i) => a <= i.u);
  const f = k.r * a * t.zc[z];
  return Math.max(f * (1 - x), t.mf);
}
// version-a/call.mjs — call site
import { calc } from "./fee.mjs";
import { s, t } from "./data.mjs";
console.log(calc(s, t, 2, 0.12).toFixed(2));

None of the four arguments at the call site explains itself. Is 2 a zone code, a tier index, a quantity? Is 0.12 a discount rate, a tax rate? The answer sits in fee.mjs; whoever is reading has to open a second file.

The Named Version

The second version performs the same arithmetic. What changes is the names, the parameter shape, and where the numbers come from.

// version-b/data.mjs — same data, with field names
export const ZONE = { SAME_CITY: 1, NEIGHBORING_ZONE: 2, DISTANT_ZONE: 3 };
export const CONTRACTED_CUSTOMER_DISCOUNT = 0.12;
export const sampleShipment = { weightKg: 2.4, widthCm: 30, lengthCm: 24, heightCm: 18 };
export const domesticTariff = {
  weightTiers: [{ upperLimitKg: 1, ratePerKg: 38 }, { upperLimitKg: 5, ratePerKg: 26 },
                { upperLimitKg: 10, ratePerKg: 21 }, { upperLimitKg: Infinity, ratePerKg: 17 }],
  zoneCoefficients: { 1: 1, 2: 1.35, 3: 1.8 },
  minimumFee: 45,
};
// version-b/fee.mjs — same calculation, with intent-revealing names
const VOLUMETRIC_DIVISOR = 3000;

export function calculateShipmentFee({ shipment, tariff, zoneCode, discountRate }) {
  const volumetricWeightKg = (shipment.widthCm * shipment.lengthCm * shipment.heightCm) / VOLUMETRIC_DIVISOR;
  const billableWeightKg = Math.max(shipment.weightKg, volumetricWeightKg);
  const tier = tariff.weightTiers.find((t) => billableWeightKg <= t.upperLimitKg);
  const zonedFee = tier.ratePerKg * billableWeightKg * tariff.zoneCoefficients[zoneCode];
  return Math.max(zonedFee * (1 - discountRate), tariff.minimumFee);
}
// version-b/call.mjs — call site
import { calculateShipmentFee } from "./fee.mjs";
import { sampleShipment, domesticTariff, ZONE, CONTRACTED_CUSTOMER_DISCOUNT } from "./data.mjs";
console.log(calculateShipmentFee({ shipment: sampleShipment, tariff: domesticTariff,
  zoneCode: ZONE.NEIGHBORING_ZONE, discountRate: CONTRACTED_CUSTOMER_DISCOUNT }).toFixed(2));

Both versions are confirmed to produce the same result.

node version-a/call.mjs
node version-b/call.mjs
133.44
133.44

The behavior is identical. The difference is only in reading cost, and that cost is countable.

Measurement

The tool below counts two things. First, the opaque arguments at a given call site. Second, taking every name in the implementation that is two letters or shorter, how many words in the domain vocabulary each one could resolve to — that is, how ambiguous the abbreviation is.

// name-measure.mjs — counts opaque call-site arguments and how many domain words an abbreviation could mean
import { readFileSync } from "node:fs";

// The library's domain vocabulary: words a name is expected to carry.
const GLOSSARY = ["coefficient", "discount", "divisor", "fee", "kilogram", "limit", "minimum",
  "rate", "shipment", "tariff", "tier", "volume", "volumetric", "weight", "zone"];

// An argument is labeled if the call text itself states which parameter it fills
// (a named field, or a variable name of at least three letters).
const labeled = (arg) => /^[A-Za-z_$][\w$]{2,}\s*:/.test(arg) || /^[A-Za-z_$][\w$]{2,}$/.test(arg);

function countOpaque(file, fnName) {
  const body = readFileSync(file, "utf8");
  const call = body.match(new RegExp(`\\b${fnName}\\(([^()]*)\\)`));
  const args = call[1].replace(/[{}]/g, "").split(",").map((s) => s.trim()).filter(Boolean);
  return { total: args.length, opaque: args.filter((a) => !labeled(a)).length };
}

function abbreviations(file) {
  const body = readFileSync(file, "utf8").replace(/^\/\/.*$/gm, "");
  const names = [...new Set(body.match(/[A-Za-z_$][\w$]*/g))].filter((a) => a.length <= 2);
  return names.map((name) => ({
    name,
    candidates: GLOSSARY.filter((g) => g.startsWith(name[0].toLowerCase())),
  }));
}

for (const [file, fn] of [["version-a/call.mjs", "calc"], ["version-b/call.mjs", "calculateShipmentFee"]]) {
  const r = countOpaque(file, fn);
  console.log(`${file.padEnd(20)} ${fn.padEnd(21)} arguments=${r.total}  opaque=${r.opaque}`);
}

console.log("\nabbreviation  candidate count  candidates");
const short = abbreviations("version-a/fee.mjs");
for (const s of short) console.log(`${s.name.padEnd(9)} ${String(s.candidates.length).padEnd(12)} ${s.candidates.join(", ") || "-"}`);
const unique = short.filter((s) => s.candidates.length === 1).length;
console.log(`\nversion-a abbreviation count = ${short.length}, resolve to one candidate = ${unique}`);
console.log(`version-b abbreviation count = ${abbreviations("version-b/fee.mjs").length}`);
node name-measure.mjs
version-a/call.mjs   calc                  arguments=4  opaque=4
version-b/call.mjs   calculateShipmentFee  arguments=4  opaque=0

abbreviation  candidate count  candidates
s         1            shipment
t         2            tariff, tier
z         1            zone
x         0            -
v         2            volume, volumetric
wd        1            weight
l         1            limit
h         0            -
a         0            -
w         1            weight
k         1            kilogram
tr        2            tariff, tier
i         0            -
u         0            -
f         1            fee
r         1            rate
zc        1            zone
mf        1            minimum

version-a abbreviation count = 18, resolve to one candidate = 10
version-b abbreviation count = 1

Four opaque arguments dropped to zero. The abbreviation table gives the second result: of eighteen short names in the abbreviated implementation, ten resolve to exactly one glossary word, five resolve to none, and three resolve to more than one. Ambiguity is not the only failure mode. w genuinely stands for weight and resolves to weight; wd stands for width but resolves to that same single candidate, weight — a confident, wrong answer. k stands for the selected tier and resolves only to kilogram, another confident, wrong answer. A single matching candidate is not proof the name is right.

The named version keeps exactly one short name: the t parameter passed to the find callback. That leftover is not a coincidence; it is the subject of the next section.

Name Length Depends on Scope

A short name is not a defect by itself. The measure is the scope the name lives in. The difference between the t inside a one-line callback and a module-level export named t is the distance a reader travels to find the definition: on the same line in the first case, in another file in the second.

A practical rule follows: a name’s length should grow with the amount of code it stays visible across. A loop counter i is defensible inside a three-line loop, not as an exported constant. The reasoning runs in reverse too: an unnecessarily long name in a narrow scope (currentShipmentVolumetricWeightValue) lengthens the line without adding information.

What a Name Should Convey

The measurement says how much is missing; it does not say which information needs adding. Four criteria do that work.

Units and type live inside the name. The difference between weightKg and weight is that a mistake mixing grams with kilograms becomes visible before the code compiles. The suffixes on widthCm, upperLimitKg, and discountRate do this work; discountRate and discountAmount cannot substitute for each other.

A name must not lie. A variable named zoneList that actually holds a mapping is worse than no name at all: whoever reads it proceeds on a false assumption. The cost of a wrong name is every conclusion drawn before the mistake is found.

One word per concept. If getTariff, fetchTariff, and retrieveTariff all appear in the same library, the reader assumes three different operations. The vocabulary stays fixed throughout the course: a shipment’s billable weight appears everywhere as billableWeightKg.

Searchability. The number 3000 appears in hundreds of places across a codebase; VOLUMETRIC_DIVISOR appears once. When the number needs to change, the name is what gets searched for. Leaving a bare number unnamed costs not only reading, but changing the code.

Summary

  • Naming is not a stylistic preference but a measurable decision: the measure is the opaque argument count, a lower bound on the questions a call site cannot answer.
  • In the abbreviated version, all four arguments were opaque; in the named version, none were. Both versions produce the same result (133.44); the difference is only in reading cost.
  • The abbreviation-resolution table gave the second measure: of eighteen short names, ten resolve to exactly one domain word, five to none, and three to more than one — and a single matching candidate is not proof the name is right.
  • Name length grows with the scope a name lives in; a short name inside a one-line callback and an exported short name are not the same thing.
  • A good name carries a unit and a type, does not lie, uses one word per concept, and is searchable; naming a bare number reduces the cost of changing code, not just reading it.

Next Step

One thing stands out in the named version: the VOLUMETRIC_DIVISOR constant needed no comment line, because its name already says what it is. In the abbreviated version, the same information could only be given through a comment. Does this observation generalize? Is the presence of a comment a sign that the code had to say something it could not say itself? The next lesson answers this by measurement: how much of a comment can be translated into a name, which comments cannot be, and at what point a comment drifting apart from the code turns into a silent error.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ Esc navigate · open · close