Felix' Ramblings
Posts by Date | Posts by Name | Recordings | Other

<< Another Quick Update & Vent
 

2026-07-18
Consider Introducing Terminology To Reduce Ambiguity While Programming

I will split this up into two parts:

For good programmers, none of what I say will be a surprise, but I want to write this down regardless, because I realized that I am severely underutilizing "consistent Terminology", causing me to run into "The More Interesting Problem" too often in my codebase.

The Boring Problem

Generally speaking, you want your function names to be self-explanatory, at least within the context it's being used in. As a negative example, consider some of the functions contained the C standard library string.h:


size_t   strcspn(const char *, const char *);
char    *strpbrk(const char *, const char *);
char    *strrchr(const char *, int);

To be fair in the context of C, this was due to a limitation of old systems, so thanks to C prioritizing backwards compatibility, the function names remain as is. Nonetheless, I couldn't tell you what these functions do without looking them up.

When working on small software projects, or when I work on a module of a large software project at work, my strategy has mostly been using long and descriptive names. While language remains inherently ambiguous, the context is often sufficient to accurately (enough) describe what a function is doing.

Here is an example:


// Example in C.
// "context" is just some sort of state for each function.

// In the module "FileExport"
void SignalWriteValue(context *Context, signal *Signal, f64 Value);

// In the module "SignalImport"
void SignalWriteValue(context *Context, signal *Signal, f64 Value);

Even though "WriteValue" is ambiguous ("Where are we writing to?"), I would argue:

Of course you can expand the function names further [0], but you get the point.

The More Interesting Problem

What happens if "intuitive" naming is no longer sufficient, either due to no strict separation of concerns/modules, or if the language used is not precise enough to describe functions?

A simple example: Imagine you were writing your own version control system / something similar to git [1]. At a glance, "create a commit" seems straight forward, but when doing the actual programming, my common approach of descriptive and longer naming fails miserably:


// ---------- Files ----------
// Imagine "file" and "path" are structs.

b32 HasFileLocally(path PathToFile);
// Are we talking about the files relevant for a user, 
// or the internal files of a version control?

void WriteFile(file File);
file ReceiveFileFromServer(void);
// - Internal file or user facing file?
// - Does this need to be written to disk?
// - If user facing: Needs conversion to internal file
// - If internal file: Needs conversion to user facing file



// ---------- Commits ----------
// Imagine "commit" & "file_list" are structs.

commit CreateCommit(file_list List);
// Is this just in memory, or does the result still need to be written to disk?

commit CreateCommitAndWriteToDisk(file_list List);
// - Does the commit still need to be pushed to the server?
// - Is the commit properly linked to previous/next commits?
//   Do other commits still need updating?
// - Is the current state of the version control updated / is this now the current commit?

This is a lesson which I, repeatedly, learn the hard way: Consider introducing terminology. It does not matter if the terminology you use are "common" words or are some magic sounding ones. Have words or phrases map to specific operations, and stick to them (even if it's just for specific scopes / modules).

In the past I resorted to trying to document these functions more extensively. For smaller projects, you can often just "suffer through" the ambiguity as well. But having and using a defined terminology makes the entire process less error prone and faster to program (and more fun!). Here is a terminology applied to the files and commits example:


// ---------- Terminology ----------
// User Files:  User facing files / user-editable files
// Blob Files:  Internal wrapper files
// Draft:     Not yet written to disk
// Write:     Written to disk, but not yet linked/appended to history
// Finalize:  Fully completed (pushed, written to disk, linked to other commits)

// Just "File" is generic, array of bytes with some extra info


// ---------- Files ----------
b32  HasUserFileLocally(path PathToUserFile);
b32  HasBlobFileLocally(path PathToBlobFile);
blob ReceiveBlobFromServer(void);

void WriteBlob(blob Blob);
void WriteUserFile(user_file UserFile);
void WriteUserFileFromBlob(blob Blob);

void WriteBlobFromBytes(file File); 
void WriteUserFileFromBytes(file File);


// ---------- Commits ----------
commit DraftCommit(user_file_list List);
commit DraftAndWriteCommit(user_file_list List);
void   FinalizeCommit(commit Commit);

Note that terminology helps for a "code style guideline", but also for compile-time checks:

Terminology Across A Codebase

Terminology also does not need to be limited to application specific usages. The most obvious example is the different implications of array/array_list/vector vs list/linked_list, I only applied terminology to primitive types like these.

But when browsing through codebases like the raddebugger, you can see this concept being applied to entire "systems", which I found really intriguing. See the their usage of key and handle:

The differences become more clear when talking about a concrete example like rendering textures in a game:

Therefore terminology can also imply entire architectural decisions of the software:

And most importantly, this key -> handle approach is not limited to just textures. It is a great application for lots of "heavy resource", whether it is:

Personal Codestyle

A small trick, which I picked up from Handmade Hero but only learned to appreciate recently, is using some sort of additional indication if a function should be for "internal use only", or if the function is "easy to fuck up".

I like the approach of appending _ to the function name to indicate that the function shouldn't be used directly. It saves me the hassle of extracting the functions into a completely distinct module, but also clearly visually indicates that the function (or maybe struct member variable) should only be used with caution.

Again using commit/file example above, here is the code indicating that the result location should be handled by the library, while still providing a way to overwrite this wish, on your own caution:


void WriteCommit(commit Commit);
void WriteCommit_(commit Commit, path OutputFile);
void WriteBlob(blob Blob);
void WriteBlob_(blob Blob, path OutputFile);

In this instance, a function like WriteBlobToPath would be easy to understand, but it wouldn't indicate this extra layer of "You probably shouln't use this".


[0]: Which doesn't stop me from producing abhorently long function names with an impressively low signal-to-noise-ratio. Unlucky.

[1]: Don't ask. It's a project of mine with very little actual utility, but it has a very annoying tendency of ambiguous operations even though it's only a few thousand lines of code. This caused me to procrastinate work because my shitty naming was painful to deal with.


<< Another Quick Update & Vent
 

Posts by Date | Posts by Name | Recordings | Other
 Felix' Ramblings