The Purpose of Abstracting
A New Semantic Level#
Edsger W. Dijkstra once wrote that “the purpose of abstracting is not to be vague, but to create a new semantic level in which one can be absolutely precise.” This statement cuts against a common misunderstanding about abstraction in software. Many engineers believe that abstraction means hiding details to make things more general or flexible. They create abstractions that accept configuration objects, take generic parameters, or provide hooks for undefined future requirements. The result is vagueness dressed up as abstraction.
Dijkstra understood something different. A good abstraction creates a new level of meaning where you can reason precisely without drowning in irrelevant details. It does not make things fuzzy or general. It makes them clear at a different scale. When you design a function or class, you choose what concepts to expose and what to hide. The goal is not maximum flexibility or minimal commitment. The goal is to establish a vocabulary where you can express ideas clearly and correctly at that particular level of thinking.
I have written countless abstractions that missed this point. They tried to be everything to everyone, accepting dictionaries of options and returning generic results that pushed interpretation onto callers. These abstractions created coupling instead of reducing it because every caller needed to understand what was hidden behind the interface. The abstraction had no semantic level of its own. It was just a pass through with extra steps.
Vague Abstractions Leak#
Consider a function like this:
function parseUserInput(value: string): any {
// Could be email, phone, date, anything really
// Caller needs to validate and handle each case
return processedValue;
}
This function claims to abstract parsing, but it has no semantic level. It does not say what it parses or what operations are valid on the result. Callers must check the type, validate the format, and handle edge cases that should have been resolved at the parsing boundary. The abstraction leaks every detail it claims to hide. You cannot reason precisely about what parseUserInput does because it does not commit to doing anything specific.
This is vagueness masquerading as abstraction. The function tries to be general by accepting anything and promising nothing. Every caller must understand implementation details to use it safely. You end up with validation scattered across the codebase, duplicate error handling, and coupling between callers and parsing logic. The abstraction has failed at its primary job, which is to create a level where you can work precisely.
Creating Precise Semantic Levels#
Now consider a different approach:
class EmailAddress {
private constructor(private readonly value: string) {}
static parse(input: string): EmailAddress {
// Validate email format
if (!this.isValid(input)) {
throw new Error('Invalid email format');
}
return new EmailAddress(input.toLowerCase());
}
getDomain(): string {
return this.value.split('@')[1];
}
toString(): string {
return this.value;
}
}
This creates a new semantic level called “valid email addresses.” At this level, you can be absolutely precise. An EmailAddress object represents a correctly formatted email. You can get its domain, convert it to a string, or use it in comparisons. Invalid email addresses cannot exist at this level because the constructor is private and the parse method enforces validation.
The abstraction commits to something specific. It says “I represent valid email addresses and nothing else.” This precision eliminates entire categories of errors. You cannot accidentally pass a phone number where an email is expected. You cannot forget to validate input because validation happens at the boundary. You cannot work with malformed data because malformed data cannot enter this semantic level.
This is what Dijkstra meant. The abstraction is not vague about what it does. It is completely precise, just at a different level. Callers think in terms of email addresses rather than string validation. The complexity has not disappeared, but it has been organized into a layer where it can be handled once and correctly.
The Configuration Dictionary Trap#
Another common pattern misses the point in a similar way:
function processFile(
path: string,
options: Dictionary<string, any>
): any {
const format = options["format"]; // "csv" | "json" | ???
const mode = options["mode"]; // "read" | "write" | ???
// Caller must know which options are valid
// and what types they should be
}
This function tries to be flexible by accepting a configuration dictionary. In practice, it forces every caller to understand which string keys are valid, what types the values should be, and which combinations make sense. The abstraction has no semantic level because it refuses to commit to what it does. It is vague about inputs, vague about operations, and vague about outputs.
Compare this to establishing clear semantic levels:
class CsvFile {
static loadFrom(path: string): CsvFile {
// Load and parse CSV file
}
readRecords<T>(): T[] {
// Return typed records
}
}
class JsonDocument {
static loadFrom(path: string): JsonDocument {
// Load and parse JSON file
}
getValue(key: string): any {
// Get value by key
}
}
Each class creates a precise semantic level for working with a specific file format. CsvFile exposes operations that make sense for CSV files. JsonDocument exposes operations that make sense for JSON. You cannot accidentally treat a CSV like JSON or pass invalid options. The types prevent errors that the configuration dictionary approach requires runtime checking to catch.
This precision reduces coupling because callers do not need to understand file format details. It improves expressiveness because the code says exactly what it does. Most importantly, it creates a level where you can reason correctly about file operations without thinking about parsing logic, encoding issues, or format quirks. Those details exist, but they exist at a different semantic level where they can be handled systematically.
Applying This to Your Work#
When you design an abstraction, ask yourself what semantic level you are creating. What concepts exist at this level? What operations make sense? What states are impossible? A good abstraction commits to answering these questions precisely.
Name things for what they mean at that level, not what they do underneath. EmailAddress names a concept, not validation logic. CsvFile names a concept, not parsing implementation. The names establish a vocabulary for reasoning at that semantic level.
Hide details that do not belong at this level. Email validation rules belong inside EmailAddress, not scattered across callers. CSV parsing logic belongs inside CsvFile, not leaked through configuration parameters. The point is not to be vague about these details. The point is to be precise about them in the right place.
Make invalid states unrepresentable. If something should not exist at your semantic level, use types and visibility to prevent its construction. Private constructors, validation in factory methods, and specific types instead of generic containers all push correctness down to the abstraction boundary.
This strategy applies whether you are writing a function, designing a class, organizing a module, or defining an API. Every abstraction creates a semantic level. Your job is to make that level precise. Not vague. Not flexible for undefined futures. Precise about what it represents and what you can do with it.
When you create abstractions that commit to clear semantic levels, you reduce coupling, improve expressiveness, and make changes easier. Your code becomes a series of well defined layers, each precise at its own scale, each hiding complexity that does not belong at that level of thinking. That is the purpose of abstracting.