Epistemic status: Exploratory + I’m not an expert here. Still, I’m reasonably confident that I’m onto something.
Let’s talk about levels of abstraction. I have an example that I think would be helpful.
It comes from the first computer science lecture that I ever attended. I think the year was 2010. I was taking this class that was a precursor to CS 101. CS 101 was the normal intro class, but it assumed you had some amount of experience with programming, whereas the class that I took assumed nothing.
Anyway, the professor asked us a question. He asked us how we would write out instructions for someone to brush their teeth. An answer would look something like this:
Put toothpaste on toothbrush
Brush teeth
Rinse mouth
Clean toothbrush
Then he would say how “put toothpaste on toothbrush” isn’t very specific. I mean, it is to a sufficiently smart human. But what if the person isn’t that smart? You’d have to be more specific about what that instruction actually means. Maybe the more specific version looks something like this:
Take toothbrush out of drawer
Take toothpaste out of drawer
Rinse toothbrush
Put toothpaste on toothbrush
But then imagine that the person you are giving these instructions to is really dumb. They don’t know how to execute the instruction of “take toothbrush out of drawer”. You have to be even more specific. Ugh, what a schlep. But let’s try it.
Open drawer
Pick toothbrush up
Put toothbrush down on counter
Close drawer
Perhaps you can see where this is going. Even these instructions can be made more specific. What does it mean to open a drawer? To pick a toothbrush up? You can dig deeper and break them down into even more specific instructions.
This is sorta how computers work. They are dumb and need really, really, really specific instructions. You have to break it down further and further and further for them until they finally get it.
But — and this is a crucial point — it is hard for us humans to think in terms of these super specific instructions. Imagine that you had to explain how to brush your teeth to someone in terms of instructions like “extend your elbow until it is at a 130 degree angle”. I don’t think I’d be able to do it. I’d get lost in the weeds. I’d lose the forest for the trees.
This right here is why god gave us abstraction. Abstraction allows us to solve this problem. To bridge that gap. We can think in terms of high level instructions like “pick toothbrush up” and the computer can think in terms of low level instructions “extend elbow 130 degrees”.
How does this work? Well, I wrote a different post covering that. In this post I want to make a few different points.
Levels of abstraction
Remember that first series of instructions we had, starting with “Put toothpaste on toothbrush”? Let’s think of that as level 1.
From there, “Put toothpaste on toothbrush” wasn’t specific enough, so we dug deeper and wrote out another series of instructions explaining how to put toothpaste on the toothbrush, starting with “Take toothpaste out of drawer”. Let’s think of that series of instructions as level 2.
And similarly, let’s think of the next series of instructions as level 3.
We can even go in the opposite direction. Ultimately, we are giving someone instructions on how to “brush your teeth”. Perhaps “brush your teeth” is part of some sort of “morning routine” instruction and is at level 0. From there you can continue the process and think about level −1, level −2, etc. I suppose you’d eventually hit a wall at something like “live life” though, so you wouldn’t be able to continue this forever.
Top-down thinking
I am a very top-down thinker and get frustrated at times living in a world where I feel like others aren’t top-down enough, so excuse a little bit of passion from me here.
Imagine that you asked someone how to brush your teeth and they responded by starting with:
Open the drawer
Pick the toothbrush up
Put the toothbrush down on the counter
...
Doesn’t that want to just make you pull your hair out?!
I mean, c’mon. What are you doing with that response? It’d be so much better if you started off giving me a high level understanding of what we need to do:
Put toothpaste on toothbrush
Brush teeth
Rinse mouth
Clean toothbrush
From there, once I have that high level understanding, we can dig into the details.
Or not. Sometimes it’s not necessary. Or it can be put off into the future until it is needed.
This all may sound obvious. If it does, good. That’s good. I’m glad it sounds obvious. You wouldn’t want to start off with “Open the drawer”. Top-down thinking is great! But I find that a lot of things aren’t top-down enough for me. Maybe it’s a founder explaining to me how their startup works. Maybe it’s a lecturer explaining a biological process. Maybe it’s someone giving me directions to a park.
This isn’t the best example, but consider recipes. Imagine that I ask you how to make fresh pasta. At a high level, the answer is something like:
Make dough
Cut dough into pasta shapes
Boil pasta shapes
I like that answer. I get it. I see what is happening at a high level.
On the other hand, imagine that the answer started off like:
Crack egg yolks into a bowl
Pour flower into a larger bowl
Add salt to larger bowl
...
We’re three steps in and I really am not seeing the bigger picture yet. You’re at too low a level of abstraction. If we continued at this level of abstraction, eventually the bigger picture would shine through, but it’d take some time. Then again, maybe it wouldn’t. It’s easy to lose the forest for the trees when you’re at too low a level of abstraction.
And it makes you feel like you’re crazy, right? You follow along diligently, step by step. You understood each step. So why are you still lost about what is going on at a high level? Shouldn’t you get it?
Top-down code organization
As a person who programs for a living, the code I’ve seen is usually of the form:
But this is backwards! Right? Shouldn’t it be top-down? When I open a file, I want to understand what is happening at a high level. I want it to be top-down. I want to think about things at the right level of abstraction, and then when I want more detail, I can scroll down to see how things work at a lower level of abstraction. Ie. if I want to see what is going on with that Popover or with cutDoughIntoPastaShapes.
Granted, I can still do this with the former style of code organization. It just means that I have to scroll down to the bottom of the file, see what function is being exported, read it, and then backtrack from there. It’s just less convenient to do that.
Importantly less convenient? I’m not sure. I feel like it is. I feel like it really adds some friction as I try to understand what is going on in a file. Maybe it’s not actually that big of a deal though.
Nested code organization
Consider that example of brushing your teeth. What would that code look like? Maybe something like this:
But this all looks very… linear. In reality, there is a nested structure, and this nested structure isn’t immediately aparent as you skim through this file. It becomes aparent as you actually read through the code in the file, but that is effortful.
Here you can better see the nested structure. Especially with the help of a text editor that collapses stuff for you.
Maybe this is a good idea? Maybe other programming languages already do this? I think I remember Haskell and Clojure doing it when I dipped my toes into them, but I’m not sure. It also might be a good idea to brainstorm better ways of communicating the nested structure of code.
Well, the standard one is probably to break things into multiple files such that each file contains a maximum of two levels of abstraction, but maybe it’d be good to brainstorm something even better than that. Sometimes you have 3+ levels of abstraction but few enough lines of code where you don’t necessarily want to create more files and directories.
Mixing levels of abstraction
In the book Clean Code, Bob Martin talks about how you shouldn’t mix levels of abstraction. As I write this post, I think I finally understand what he means. Previously my understanding was more vague.
Consider again that tooth brushing example. Imagine that the code was started off something like this:
That’s confusing, right? You know that the overall goal is to have a brushTeeth function. You see that it starts off by putting toothpaste on the toothbrush. So far so good. But then it starts talking about moving arms and squeezing fingers. Huh? Who threw that wrench in there?
The problem is that arms and fingers are the wrong level of abstraction. If it was a graspHandle function or something like that, we might have the context to read some code about moving arms and fingers and understand what it is doing. But in the context of brushing ones teeth, it’s hard to understand what is going on when you jump to such a low level of abstraction.
But I already made this point before about how it’s hard to understand things when you use a level of abstraction that is too low. This point is a subtley different one. In addition to it being too low, it is also a problem that it is being mixed with other levels of abstraction.
Actually, I’m not sure of that. Maybe the issue always boils down to it being too low a level of abstraction, not that the levels are being mixed. Nevertheless, I think we can agree that mixed levels of abstraction is a code smell to be aware of.
Speaking of which, friendly reminder: code smells are guidelines, not hard rules. Sometimes you notice a smell but judge that in practice it makes sense to keep it as is. Here, I think that there are probably times when it is ok to mix levels of abstraction a little bit.
Assorted thoughts about abstraction
Epistemic status: Exploratory + I’m not an expert here. Still, I’m reasonably confident that I’m onto something.
Let’s talk about levels of abstraction. I have an example that I think would be helpful.
It comes from the first computer science lecture that I ever attended. I think the year was 2010. I was taking this class that was a precursor to CS 101. CS 101 was the normal intro class, but it assumed you had some amount of experience with programming, whereas the class that I took assumed nothing.
Anyway, the professor asked us a question. He asked us how we would write out instructions for someone to brush their teeth. An answer would look something like this:
Put toothpaste on toothbrush
Brush teeth
Rinse mouth
Clean toothbrush
Then he would say how “put toothpaste on toothbrush” isn’t very specific. I mean, it is to a sufficiently smart human. But what if the person isn’t that smart? You’d have to be more specific about what that instruction actually means. Maybe the more specific version looks something like this:
Take toothbrush out of drawer
Take toothpaste out of drawer
Rinse toothbrush
Put toothpaste on toothbrush
But then imagine that the person you are giving these instructions to is really dumb. They don’t know how to execute the instruction of “take toothbrush out of drawer”. You have to be even more specific. Ugh, what a schlep. But let’s try it.
Open drawer
Pick toothbrush up
Put toothbrush down on counter
Close drawer
Perhaps you can see where this is going. Even these instructions can be made more specific. What does it mean to open a drawer? To pick a toothbrush up? You can dig deeper and break them down into even more specific instructions.
This is sorta how computers work. They are dumb and need really, really, really specific instructions. You have to break it down further and further and further for them until they finally get it.
But — and this is a crucial point — it is hard for us humans to think in terms of these super specific instructions. Imagine that you had to explain how to brush your teeth to someone in terms of instructions like “extend your elbow until it is at a 130 degree angle”. I don’t think I’d be able to do it. I’d get lost in the weeds. I’d lose the forest for the trees.
This right here is why god gave us abstraction. Abstraction allows us to solve this problem. To bridge that gap. We can think in terms of high level instructions like “pick toothbrush up” and the computer can think in terms of low level instructions “extend elbow 130 degrees”.
How does this work? Well, I wrote a different post covering that. In this post I want to make a few different points.
Levels of abstraction
Remember that first series of instructions we had, starting with “Put toothpaste on toothbrush”? Let’s think of that as level 1.
From there, “Put toothpaste on toothbrush” wasn’t specific enough, so we dug deeper and wrote out another series of instructions explaining how to put toothpaste on the toothbrush, starting with “Take toothpaste out of drawer”. Let’s think of that series of instructions as level 2.
And similarly, let’s think of the next series of instructions as level 3.
We can even go in the opposite direction. Ultimately, we are giving someone instructions on how to “brush your teeth”. Perhaps “brush your teeth” is part of some sort of “morning routine” instruction and is at level 0. From there you can continue the process and think about level −1, level −2, etc. I suppose you’d eventually hit a wall at something like “live life” though, so you wouldn’t be able to continue this forever.
Top-down thinking
I am a very top-down thinker and get frustrated at times living in a world where I feel like others aren’t top-down enough, so excuse a little bit of passion from me here.
Imagine that you asked someone how to brush your teeth and they responded by starting with:
Open the drawer
Pick the toothbrush up
Put the toothbrush down on the counter
...
Doesn’t that want to just make you pull your hair out?!
I mean, c’mon. What are you doing with that response? It’d be so much better if you started off giving me a high level understanding of what we need to do:
Put toothpaste on toothbrush
Brush teeth
Rinse mouth
Clean toothbrush
From there, once I have that high level understanding, we can dig into the details.
Or not. Sometimes it’s not necessary. Or it can be put off into the future until it is needed.
This all may sound obvious. If it does, good. That’s good. I’m glad it sounds obvious. You wouldn’t want to start off with “Open the drawer”. Top-down thinking is great! But I find that a lot of things aren’t top-down enough for me. Maybe it’s a founder explaining to me how their startup works. Maybe it’s a lecturer explaining a biological process. Maybe it’s someone giving me directions to a park.
This isn’t the best example, but consider recipes. Imagine that I ask you how to make fresh pasta. At a high level, the answer is something like:
Make dough
Cut dough into pasta shapes
Boil pasta shapes
I like that answer. I get it. I see what is happening at a high level.
On the other hand, imagine that the answer started off like:
Crack egg yolks into a bowl
Pour flower into a larger bowl
Add salt to larger bowl
...
We’re three steps in and I really am not seeing the bigger picture yet. You’re at too low a level of abstraction. If we continued at this level of abstraction, eventually the bigger picture would shine through, but it’d take some time. Then again, maybe it wouldn’t. It’s easy to lose the forest for the trees when you’re at too low a level of abstraction.
And it makes you feel like you’re crazy, right? You follow along diligently, step by step. You understood each step. So why are you still lost about what is going on at a high level? Shouldn’t you get it?
Top-down code organization
As a person who programs for a living, the code I’ve seen is usually of the form:
instead of:
Especially with ReactJS components. It’d be like:
instead of:
But this is backwards! Right? Shouldn’t it be top-down? When I open a file, I want to understand what is happening at a high level. I want it to be top-down. I want to think about things at the right level of abstraction, and then when I want more detail, I can scroll down to see how things work at a lower level of abstraction. Ie. if I want to see what is going on with that
Popover
or withcutDoughIntoPastaShapes
.Granted, I can still do this with the former style of code organization. It just means that I have to scroll down to the bottom of the file, see what function is being exported, read it, and then backtrack from there. It’s just less convenient to do that.
Importantly less convenient? I’m not sure. I feel like it is. I feel like it really adds some friction as I try to understand what is going on in a file. Maybe it’s not actually that big of a deal though.
Nested code organization
Consider that example of brushing your teeth. What would that code look like? Maybe something like this:
But this all looks very… linear. In reality, there is a nested structure, and this nested structure isn’t immediately aparent as you skim through this file. It becomes aparent as you actually read through the code in the file, but that is effortful.
What if we did something like this instead?
Here you can better see the nested structure. Especially with the help of a text editor that collapses stuff for you.
Maybe this is a good idea? Maybe other programming languages already do this? I think I remember Haskell and Clojure doing it when I dipped my toes into them, but I’m not sure. It also might be a good idea to brainstorm better ways of communicating the nested structure of code.
Well, the standard one is probably to break things into multiple files such that each file contains a maximum of two levels of abstraction, but maybe it’d be good to brainstorm something even better than that. Sometimes you have 3+ levels of abstraction but few enough lines of code where you don’t necessarily want to create more files and directories.
Mixing levels of abstraction
In the book Clean Code, Bob Martin talks about how you shouldn’t mix levels of abstraction. As I write this post, I think I finally understand what he means. Previously my understanding was more vague.
Consider again that tooth brushing example. Imagine that the code was started off something like this:
That’s confusing, right? You know that the overall goal is to have a
brushTeeth
function. You see that it starts off by putting toothpaste on the toothbrush. So far so good. But then it starts talking about moving arms and squeezing fingers. Huh? Who threw that wrench in there?The problem is that arms and fingers are the wrong level of abstraction. If it was a
graspHandle
function or something like that, we might have the context to read some code about moving arms and fingers and understand what it is doing. But in the context of brushing ones teeth, it’s hard to understand what is going on when you jump to such a low level of abstraction.But I already made this point before about how it’s hard to understand things when you use a level of abstraction that is too low. This point is a subtley different one. In addition to it being too low, it is also a problem that it is being mixed with other levels of abstraction.
Actually, I’m not sure of that. Maybe the issue always boils down to it being too low a level of abstraction, not that the levels are being mixed. Nevertheless, I think we can agree that mixed levels of abstraction is a code smell to be aware of.
Speaking of which, friendly reminder: code smells are guidelines, not hard rules. Sometimes you notice a smell but judge that in practice it makes sense to keep it as is. Here, I think that there are probably times when it is ok to mix levels of abstraction a little bit.