One of the fastest ways to become an expert and generate novel ideas is to approach a topic from an unrelated angle.
Want to learn how to program but have never studied it? Approach programming armed with another vocabulary. Whether it's carpentry, biology, or math, each gives you tools and models for understanding. Not just for understanding, but contributing. The best ideas are the result of combining unrelated thoughts and techniques.
So, I want to know the answer to the following question:
What is the object of software design?
I’ll approach this question by relating ideas in philosophy, specifically metaphysics and philosophy of language, to software. I’ll start by introducing distinctions from philosophy and then look at their counterparts in software design. Then answer the question.
Three Levels of Language, Three Levels of Code
Suppose you want to build a meditation app, like Stoa. You’ll start with feature specs, such as enabling users to listen to and track meditations. Once there’s sufficient clarity here, move onto designing the data model. Start by asking, what are the fundamental pieces in the system? Meditations, users, playlists, courses? What are the properties of each of these components? At this step, you’re doing code design. And you’re making important decisions, decisions that could impact you, other developers, and users for years.
But what is the object of design? Another way to ask this is: when you’re designing software, what are you designing? The obvious answer is "code" or “software” — but that's imprecise. A more precise answer offers a better way to think about code design, helping make better decisions.
To show why, let's start with three related but distinct ideas from the philosophy of language: utterances, sentences, and propositions.
What do these mean?
An utterance refers to the saying of a sentence. If I say, "It looks like Dion is programming in Ruby," that would be an utterance of the English sentence. If I message my friend, "Dion is programming in Ruby," that would count as an utterance as well.
Sentences are familiar linguistic items. This is a sentence! Next.
Think of a proposition as what is expressed by a sentence. When I say that Dion is programming in Ruby, then there's a state of affairs represented, namely the state of affairs where Dion is programming Ruby. The proposition is either true or false. Dion is either programming Ruby or not. I can also express the same proposition with a distinct sentence in a different language: "Dion está programando en Ruby." Propositions are not sentences.
So speech has three levels: utterance, sentence, and proposition. You can think of code as having three levels that map onto the three levels of speech: execution, concrete representation, and abstract representation.
At the first level, the level of execution, code runs with specific parameters and runtime values. Suppose a PATCH
request triggers Dion’s endpoint: meditation/:meditation_id/edit
. The :meditation_id
and request data determine which operations are performed. As the code is executed, it will carry the values (and likely assign new ones) through its execution.
The execution level of code corresponds to the utterance of a sentence. Execution is an instantiation of code (a concrete representation); an utterance is an instantiation of a sentence.
At the second level, you have concrete representation. Here, think of the written code. For example, if you investigate Dion's web app, you'll find that it has specific classes, methods, and so forth. This representation ignores runtime values and particular parameters. For example, the update endpoint for meditations doesn't assume that :meditation_id
is a specific number. You can update a Meditation
with any valid id.
This level is concrete because it includes implementation details like the language and syntax of a program. It can be instantiated on different machines, at different times, with different parameters and runtime values. Similarly, a sentence can be uttered in different ways, at different times, in different contexts. Hence, the concrete representation corresponds to a sentence.
Here's a simple example:
class Api::MeditationController
def update
meditation = Meditation.find(meditation_id)
if meditation.update(meditation_attributes)
render json: meditation.as_json
else
render json_error
end
end
end
At the level of execution, this code will have specific values. For example, @meditation
will be assigned a particular Meditation
object (if one exists), and @meditation.update
will have a return value. At the level of concrete representation, we're considering the code as you see it — abstracting away the specific values it may have during execution.
At the third level, there's what I’ll call an abstract representation. Dion's code represents abstract objects like a Meditation
. Because a Meditation
is abstract, they can be represented by code in another language. In this example, you can think of a Meditation
as a cluster of properties represented by a type definition:
interface Meditation {
createdAt: DateTime,
updatedAt: DateTime,
title: String,
description: String,
...
}
Note that this is describing a language-independent object. One abstract representation can have many concrete representations. Additionally, one concrete representation can have many abstract representations. For example, consider:
const isEligible = person => {
person?.account?.credit > N
}
This function may be checking whether a person has enough financial credit in their bank account or checking whether a person is eligible for a reward based on a social credit system in a video game. We can't tell. This is unsurprising since sentences can also represent different propositions.
An abstract representation can have many different concrete representations. Language choice and syntax is an implementation detail. Likewise, different sentences in different languages may express the same propositions.
So we have the relations between the three ideas:
Utterance => Execution
Sentence => Concrete Representation
Proposition => Abstract Representation
The Object Of Design
Returning to the original query, what is the object of software design? Some programmers spend most of their time concerned with concrete representation. They'll ask questions like: is my code well-formed? Does it produce the right results when executed? Will it continue to deliver the right results when run with different parameters, in a different context?
This is unlike most philosophical work. The object of philosophy is the world, as represented by propositions. A philosopher wants to know whether invertebrates are conscious, what the nature of explanation is, or whether it's permissible to let others do wrong. They aren't concerned with the analysis of sentences. They're concerned with the propositions the sentences express and whether those states of affairs hold.
Well, that's not entirely right. Ordinary language philosophers sought to answer philosophical questions by looking at our use of sentences. A slogan of the ordinary language philosopher is: use is meaning. This philosopher spends most of their time at level two, the sentence level. They ask questions like, is my sentence meaningful? Will it communicate what I want to in the relevant contexts? Will other language users understand it? And most importantly, they answer philosophical questions by looking at how language is used. Essentially, the programmer who spends most of their time at the level of concrete representation and execution is like the ordinary language philosopher. They're not concerned with what code abstractly represents. They just want it to work!
In philosophy, the ordinary language approach is controversial. Though several philosophical debates and puzzles have the appearance of language games. To name a few:
The Ship of Theseus: if we completely remove and replace the parts of a ship over a year, does the original ship persist, or is there a new ship at the end?
Puzzles over vagueness: if I slowly remove hair from a person's head, is there a precise moment when they are bald?
Swamp man: if a creature was magically zapped into existence who had all the same structural properties as you, would they have beliefs about the world? Or do beliefs require a causal history?
Composition: Do particles that are arranged tablewise (in a table shape) form some new additional thing, a table?
The ordinary language philosopher believes that we need to answer (or defuse) these questions by analyzing the language that creates them. While I'm sympathetic towards thinking that each of these questions isn’t substantive, that is a different piece.
More importantly, this controversy doesn't exist in software. Some philosophical questions may solely concern language, but programs are typically about the concrete world. Programmers write apps to move money, analyze behavior, transmit speech, move people from a to b, and more.
Programming As Metaphysics
Ted Sider opens Writing The Book Of the World with the following picture:
There are two ways to divide up this world: as a square with two sides red and blue or as a square with two sides diagonal left and diagonal right:
Dividing up the world into diagonal left and diagonal right is strange. Yet there's a sense in which one wouldn't be making a mistake: you can accurately describe the square by reference to its two sides, diagonal left, and diagonal right. One wouldn't be making a mistake at the level of propositions or sentences.
However, the second picture doesn't "carve reality at its joints." It doesn't describe the structure of the world properly. The square's structure is properly described by red and blue; the diagonal left and diagonal right are artificial constructions. Another way to put this is that diagonal left and right are not the right abstract representations.
One way to view metaphysics is as the project of carving reality at its joints. Metaphysicians are aiming for theories that describe the fundamental structure, nature, or entities in the world. They’re trying to develop an account of the world that properly conceptualizes the way it is.
Software design is the same.
Let’s return to the meditation app. Consider the question, how should I represent a meditation in a program? What is the right abstract representation?
A simple design would be something like what we've already seen:
interface Meditation {
createdAt: DateTime,
updatedAt: DateTime,
courseId: Integer,
title: String,
description: String,
filePath: String,
meditationLengthSec: Integer,
}
This describes the primary parts of a meditation as a Meditation
object. You can imagine loading these into an app and letting users play them. However, it may be too broad of a brush. It combines meditation audio and a meditation into a single thing - how would you change this object if you wanted to allow users to change a Meditation
's length or speaker? This interface assumes that a meditation has a single file path or length. Yet, these are properties of MeditationAudio
, not a Meditation
. Retroactively introducing MeditationAudios
would be easy at the interface level, but it would be costly to refactor consumers that assume every Meditation
has only one MeditationAudio
.
Like Sider's square example, you can assume that the original interface is correct. If you want users to be able to toggle lengths, you could even avoid creating MeditationAudio
with:
interface Meditation {
createdAt: DateTime,
updatedAt: DateTime,
courseId: Integer,
title: String,
description: String,
filePath: String,
meditationLengthSec: Integer,
filePathII: String,
meditationLengthSecII: Integer
}
Note that this will work, but seems inelegant. By treating a Meditation
object as something to add arbitrary properties to the program's ontology is unrestricted. It works but isn't carving the world at its joints, like the diagonal world. A better way to do this would be to explicitly distinguish the Meditation
from a MeditationAudio
object:
interface Meditation {
createdAt: DateTime,
updatedAt: DateTime,
courseId: Integer,
title: String,
description: String,
}
interface MeditationAudio {
createdAt: DateTime,
updatedAt: DateTime,
meditationId: Integer,
title: String,
filePath: String,
lengthSec: Integer,
narratorId: Integer,
}
Now that Meditations
have MeditationAudios
, the data model easily extends to handle the case where a single meditation has multiple audio files or narrators.
Consider two other examples. Suppose you're creating a simple goal tracking app. How should goals and users be related? It depends on the app's purpose, but if it's an app for individuals to privately track their progress, goals should belong to a single user. Now suppose you're creating the next fintech app: you have users and their financial accounts. How should one relate users and accounts? Initially, the answer may be the same; accounts should belong to a single user. But this doesn't adequately describe what financial accounts are. There can be joint accounts! Likely, at the beginning of your startup's lifecycle, you can skate by without them. Later on, it will cost you time and money to change.
The upshot of this is that code design isn't merely getting the internals of a program to fit together. It's about properly conceptualizing the objects that you're working with. Doing this well prevents programmer pain, saves money, and can be sublime. The object of design is the world, not merely code. Programming as metaphysics.
Read the computer science version of these ideas here: The Three Levels of Software by Jimmy Koppel. Recommended.
Thanks to Elaine Lin, Jimmy Koppel, Austin Wilson, Will Larson and fellows from the On Deck Writer Fellowship for reading earlier versions of this essay.