Hello all! It's been a while. In the last 2 months, I've submitted 3 papers, visited 4 European universities, and began teaching my 10th cohort of the web course. On the other hand, I've already failed on my goal to write one newsletter a month this year, and decided to write you an extra-good one. Many hours later, I present my next.....blog post.
Things that aren't abstraction
Interfaces are abstractions
— Olaf Thielke, the "Code Coach"
Interfaces are not abstractions
— Mark Seeman, author of Code that Fits in Your Head and Dependency Injection
In software, abstraction is the process of simplifying code by finding similarities between different parts of the code and extracting shared logic into a named component (such as a function, module, etc...), thereby reducing the amount of code required.
Abstraction is a process which is used to produce generalized concepts or models, but in software we often refer to individual parts of a model as abstractions as well.
— Steve Smith, author of Architecting Modern Web Applications with ASP.NET Core and Microsoft Azure
Of all the concepts debated in software engineering, abstraction is at the top. I found two separate debates about it on Twitter from the past week.
As the quoted writers show, people do not even agree what abstraction means. Abstraction seems to stand for a hodgepodge of different concepts involving generality, vagueness, or just plain code reuse. These engineering debates about whether duplications are better than the wrong abstraction or about whether abstraction makes card harder to read turn into actual arguments over code. But this confusion makes all such debates doomed.
This situation particularly sad for me as someone with a background in PL theory. There are a lot of topics in software engineering that are the result of accumulated intuition over decades. But we've had a pretty good definition of abstraction since 1977, and it actually translates quite well to engineering.
Abstraction is usually said to be something which helps readers understand code without delving into the details. From this postulate, I take the position that software engineers will benefit from studying the mathematics of this kind of abstraction, understanding how it explains things they already do, and letting this coherent definition rule their use of the term "abstraction." But my goal in this newsletter is a smaller enabling step: to give you names for the other concepts that are often combined under the name "abstraction" that they may be referenced, used, and critiqued specifically, and to help you move away from citrus advice.
There are at least four other things that go under the name abstraction.
One contender for the oldest programming language is the lambda calculus, where Alonso Church showed us that, by copying symbols on pen-and-paper using a single rule, one could compute anything.
In the lambda calculus, making a new function is called "lambda abstraction," and often just "abstraction."
It's been noted that this use of the word "abstraction" is quite different from other uses. Unfortunately, this has polluted broader discussion. Even though the lambda calculus usage is akin to adding an opening and closing brace to a block of code, this usage leaks out into discussion of "abstracting things into functions" and from there into extolling the benefits of being able to ignore details and other things that closing braces really don't let you do.
So functions are abstractions — just in a very limited meaning of the word with little relation to everything else under that label. Moving on...
"Anti-unification" is a fancy term for the process of taking two things that are mostly similar, and replacing the different parts with variables. If you substitute those variables one way, you get the first thing back; else you get the second. If you see x*x and (a-b)*(a-b) near each other in a codebase and extract out a square function, then you've just done an anti-unification (getting the pattern A*A with the two substitutions [A |-> x] and [A |-> a*b]).
(Its opposite is unification, which is comparatively never used in programming. Unless you're writing Prolog, in which case, it's literally on every single line.)
This probably looks familiar: virtually all of what goes under the Don't Repeat Yourself label is an example of anti-unification. Perhaps you would describe the above as "abstracting out the square function" (different from the previous definition, where "abstracting" is just adding curly braces).
But it's also identical to the definition of abstraction given by Eric Elliott's definition above, who then goes on to claim "The secret to being 10x more productive is to gain a mastery of abstraction" (source). That sounds pretty impressive for a process which was first automated in 1970.
"Boxing" is what happens when you do too much anti-unification: a bunch of places with syntactically-similar code turns into one big function with lots of conditionals and too many parameters. "Boxing" is a term of my own invention, though I can't truly claim credit, as the "stuffing a mess into a box" metaphor predates me. Preventing this is exactly the concern expressed in the line "duplication is better than the wrong abstraction," as clarified by a critic.
There's a surefire sign that boxing has occurred. Sandi Metz describes it nicely:
Programmer B feels honor-bound to retain the existing abstraction, but since isn't exactly the same for every case, they alter the code to take a parameter, and then add logic to conditionally do the right thing based on the value of that parameter.
I've written and spoken against this kind of naive de-duplication before. One of the first exercises in my course is to give two examples of code that have the same implementation but a different spec (and should therefore evolve differently). Having identical code is not a foolproof measure that two blocks do the same thing, and it's helpful to have different terminology for merging similar things that do and do not go together.
But, particularly, if we want abstraction to have something to do with being able to ignore details, we have to stop this scenario "abstraction."
Though not precisely defined, indirection typically means "any time you need to jump to another file to see what's going on." It's commonly associated with Enterprise Java, thanks to books such as Fowler's Patterns of Enterprise Application Architecture, and is exemplified by the Spring framework and parodied by FizzBuzz: Enterprise Edition. This is where you get people complaining about "layers" of abstraction. It commonly takes the form of long chains of single-line functions calling each other or class definitions split into a hierarchy across 7 files.
Fowler's examples include abstracting
into the Service Layer Abstraction (TM)
If you were to describe the former, you'd probably say "It calculates the recognized revenue for the contract." If you were to describe the latter, you'd probably say "It calculates the recognized revenue for the contract." We've been spared no details.
In programming language theory and formal methods, there are several definitions of "abstraction" in different contexts, but they are all quite similar: abstractions are mappings between a messy concrete world and a clean idealized one. For concrete data types, an abstraction maps a complicated data structure to the basic data it represents. For systems, an abstraction relates the tiny state changes from every line implementing a TCP stack with the information found in the actual protocol diagram. These abstractions become useful when one can define interesting operations purely on the abstract form, thus achieving the dictum of Dijkstra, that "The purpose of abstraction is not to be vague, but to create a new semantic level in which one can be absolutely precise."
You've probably used one such abstraction today: the number 42 can be represented in many forms, such as with bits, tally marks, and even (as is actually done in mathematics) as a set. Addition has correspondingly many implementations, yet you need not think of any of them when using a calculator. And they can be composed: there is an abstraction to the mathematical integers from their binary implementation, and another abstraction to binary from the actual voltages. In good abstractions, you'll never think that it's even an abstraction.
So, what do abstractions actually look like in code?
Where are the abstractions?
A running joke in my software design course is that, whenever I show some code and ask whether it has some design property, the answer is always "maybe." And so it is whenever you ask "Is X an abstraction of Y?"
Abstractions are mappings. An abstraction is a pattern we impose on the world, not the entities it relates.
So instead we ask: "Is there an abstraction from X to Y?"
I can say that integers are a great abstraction of the hardware used to actually store them in RAM. Each number corresponds to a large set of different possible voltages on many transistors, and adding two numbers corresponds to many different related ways of shifting electric charge. With the right knowledge, one could write this down, though it's nowhere explicitly in the symptom.
And a starting corollary: I can abstract your TV into an integer. Simple: just take its serial number.
This does not mean that integers are an abstraction of TV sets, just that this particular mapping is. But it is not a good one, for it does not preserve any of the operations of TV sets, nor is there any similar mapping between the normal operations of integers and those of TVs. Adding one to a serial number does not give you the serial number for a larger TV.
And this means that abstractions are usually easy to find. There is indeed an abstraction from every implementation to its interface, and to every function call from its implementation. But only the good ones allow us to look at a chain of interface calls and make predictions beyond "something happens."
It means that, rather than looking at whether a piece of code uses function or interface syntax and declaring whether it brings forth the touted benefits and criticized drawbacks of "abstractions," we must think more deeply and identify exactly how the messy world is being transformed into a clean ideal.
And it means that we must look beyond the binary of whether something is or is not an abstraction, and instead discover the new semantic level on which we can be absolutely precise.
Our $40,000 grant for AI tutoring
I'm pleased to announce that that I'm a recipient of an ACX Grant, awarded by famed blogger Scott Alexander. In Scott's words:
Jimmy Koppel, $40,000, to support his work on intelligent tutoring systems. We know 1-on-1 tutoring is the best way to learn, but human tutoring doesn't scale to the number of students who need it. Computer tutoring systems can ask questions, identify areas where people need to improve, and notice/respond to specific error patterns. I was originally skeptical about this but reading things like this essay have gotten me excited. Pure AI tutoring is hard because "it takes 300 hours to develop 1 hour of intelligent tutoring system curriculum", so Jimmy is working on a hybrid model where computers do lots of the work but there's still a human in the loop. Jimmy has a PhD in computer science from MIT and currently runs a company doing advanced training for professional software engineers.
Our primary goal is to reduce the cost of giving personalized feedback by 3x-10x. We've been working hard on getting an optimized interface, and have some early promising results on the AI side. We'll share more when we have a demo.
We'll be open-sourcing the core of the technology. We are interested in connecting with tutors and instructors in other subjects who can make use of it.
The next cohort of the Advanced Software Design Web Course starts April 12th and is now accepting applications. We're running more cohorts this year, but that doesn't necessarily make it easier to get a spot: both of the last two cohorts (October and February) were sold out without ever being properly announced.