(Cross posted on my personal blog.)

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:

  1. Put toothpaste on toothbrush
  2. Brush teeth
  3. Rinse mouth
  4. 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:

  1. Take toothbrush out of drawer
  2. Take toothpaste out of drawer
  3. Rinse toothbrush
  4. 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.

  1. Open drawer
  2. Pick toothbrush up
  3. Put toothbrush down on counter
  4. 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:

  1. Open the drawer
  2. Pick the toothbrush up
  3. Put the toothbrush down on the counter
  4. ...

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:

  1. Put toothpaste on toothbrush
  2. Brush teeth
  3. Rinse mouth
  4. 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:

  1. Make dough
  2. Cut dough into pasta shapes
  3. 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:

  1. Crack egg yolks into a bowl
  2. Pour flower into a larger bowl
  3. Add salt to larger bowl
  4. ...

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:

const makeDough = () => { ... };
const cutDoughIntoPastaShapes = () => { ... };
const boilPastaShapes = () => { ... };
const makeFreshPasta = () => {
  makeDough();
  cutDoughIntoPastaShapes();
  boilPastaShapes();
};

export default makeFreshPasta;

instead of:

const makeFreshPasta = () => {
  makeDough();
  cutDoughIntoPastaShapes();
  boilPastaShapes();
};
const makeDough = () => { ... };
const cutDoughIntoPastaShapes = () => { ... };
const boilPastaShapes = () => { ... };

export default makeFreshPasta;

Especially with ReactJS components. It'd be like:

const Button = () => { ... };
const Input = () => { ... };
const Popover = () => { ... };
const ConfidenceSubmission = () => (
  <section>
    <Button />
    <Input />
    <Popover />
  </section>
);

export default ConfidenceSubmission;

instead of:

const ConfidenceSubmission = () => (
  <section>
    <Button />
    <Input />
    <Popover />
  </section>
);
const Button = () => { ... };
const Input = () => { ... };
const Popover = () => { ... };

export default ConfidenceSubmission;

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:

const brushTeeth = () => {
  putToothpasteOnToothbrush();
  scrubTeeth();
  rinseMouth();
  cleanToothbrush();
};

const putToothpasteOnToothbrush = () => {
  takeToothbrushOutOfDrawer();
  takeToothpasteOutOfDrawer();
  rinseToothbrush();
  putToothpasteOnToothbrush();
};

const takeToothbrushOutOfDrawer = () => {
  openDrawer();
  pickToothbrushUp();
  putToothbrushDownOnCounter();
  closeDrawer();
};

const openDrawer = () => { ... };

const pickToothbrushUp = () => { ... };

const putToothbrushDownOnCounter = () => { ... };

const closeDrawer = () => { ... };

const takeToothpasteOutOfDrawer = () => { ... };

const rinseToothbrush = () => { ... };

const putToothpasteOnToothbrush = () => { ... };

const scrubTeeth = () => { ... };

const rinseMouth = () => { ... };

const cleanToothbrush = () => { ... };

export default brushTeeth;

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?

const brushTeeth = () => {
  putToothpasteOnToothbrush();
  scrubTeeth();
  rinseMouth();
  cleanToothbrush();

  // let's imagine that hoisting worked here
  const putToothpasteOnToothbrush = () => {
    takeToothbrushOutOfDrawer();
    takeToothpasteOutOfDrawer();
    rinseToothbrush();
    putToothpasteOnToothbrush();

    const takeToothbrushOutOfDrawer = () => {
      openDrawer();
      pickToothbrushUp();
      putToothbrushDownOnCounter();
      closeDrawer();

      const openDrawer = () => { ... };
      const pickToothbrushUp = () => { ... };
      const putToothbrushDownOnCounter = () => { ... };
      const closeDrawer = () => { ... };
    };
    const takeToothpasteOutOfDrawer = () => { ... };
    const rinseToothbrush = () => { ... };
    const putToothpasteOnToothbrush = () => { ... };
  };
  const scrubTeeth = () => { ... };
  const rinseMouth = () => { ... };
  const cleanToothbrush = () => { ... };
};

export default brushTeeth;

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:

const brushTeeth = () => {
  putToothpasteOnToothbrush();
  moveArm(130, 'degrees');
  squeezeFingers();
  moveArm(5, 'backwards);
  ...
  rinseMouth();
  ...
};

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.

16

9 comments, sorted by Click to highlight new comments since: Today at 2:05 PM
New Comment

I agree with your preferences for top-down abstraction description. Sadly, simple text editors are not designed for it, though various IDEs show a popup of a method body when clicked or moused over. None do it recursively though, I think. 

I'm not sure what you mean. You can write this (top-down):

const ConfidenceSubmission = () => (
  <section>
    <Button />
    <Input />
    <Popover />
  </section>
);
const Button = () => { ... };
const Input = () => { ... };
const Popover = () => { ... };

export default ConfidenceSubmission;

instead of this (bottom-up)

const Button = () => { ... };
const Input = () => { ... };
const Popover = () => { ... };
const ConfidenceSubmission = () => (
  <section>
    <Button />
    <Input />
    <Popover />
  </section>
);

export default ConfidenceSubmission;

in any editor, right?

Yes, but you can't just see the top-level abstraction ConfidenceSubmission, then mouse over it, which pops up the lambda of it, then mouse over say, Button and see its lambda, and so on.

I'm able to do that in VSCode when I hover over while holding down the option key.

I believe you, but I don't think it allows seamless zooming in and out of abstraction levels, including editing and auto-refactoring in place... Maybe there is a plugin for that.

Not a programmer, but I think one other reason for this is that at least in certain languages (I think interpreted languages, e.g. Python, is the relevant category here), you have to define a term before you can use it; the interpreter basically executes the code top-down instead of compiling it first, so it can't just look later in the file to figure out what you mean. So

def brushTeeth():
    putToothpasteOnToothbrush()
    ...

def putToothpasteOnToothbrush():
    ...


wouldn't work, because you're calling putToothpasteOnToothbrush() before you've defined it.

Yeah sometimes that is a problem for sure. In various situations it's not a problem though. There's a thing called hoisting where if you have:

sayHi("Adam");

function sayHi(name) {
  console.log(`Hi ${name}`);
}

sayHi will be moved to the top of the file above sayHi("Adam") when the code is executed, even though it is written below it.

The other way I know of that this dilemma is solved is closures. Imagine that we had a makeFreshPasta.js file with this:

const makeFreshPasta = () => {
  makeDough();
  cutDoughIntoPastaShapes();
  boilPastaShapes();
};
const makeDough = () => { ... };
const cutDoughIntoPastaShapes = () => { ... };
const boilPastaShapes = () => { ... };

export default makeFreshPasta;

The goal of the file is to export a makeFreshPasta function. It's ok that the makeFreshPasta function uses functions like makeDough that are defined below it because of how closures work. Basically, the function body of makeFreshPasta will always have access to anything that was in scope at the time makeFreshPasta was defined.

I'm not sure about other languages, but I'm sure there are other solutions available for writing code that follows the top-down style.

So that is my response to the practical question of how to write code that is top-down. But I am also making a point about how I think things should be. So even if it weren't possible to write this sort of top-down code, my prescriptive point of "this is how it should be possible to write the code" still stands.

New to LessWrong?