Refactoring for Machine Dignity

So I'm going to make a bold claim. We should treat the machines that execute our programs with the same dignity as we would hope for ourselves or other people in performing that task. I'm going to start this wacky, so strap in.

Artificial Consciousness

Okay, so you probably don't believe that computers running your programs are sentient and have feelings and what not. That's ok. Yes, there are some interesting ideas about how we could know/don't know/can't know, but that's not central here. I'd like to know what we give up by making that assumption when we write code.

I'd say we give up very little. We might end up with a few weird looks and conversations.

"But like, do you really believe that...?"

"lol idk" will probably work fine. Does for me.

Ok. Moving on. Obviously the detriments are not so great (in most situations) for holding these beliefs (at least temporarily). But what do you gain?

Better Code

No, if you think you're computer is a sentient being you won't automatically write better code. But here's a handful of ideas of how to improve your code:

  • Get more experience
  • Write in a different language
  • Write with a different framework
  • Study Math
  • Study Design Patterns
  • Study Algorithmic Complexity
  • Study Good Code
  • Avoid antipatterns

You might be thinking that these are all super vague, and that's totally true. In addition to these, there are a bunch of practices codified in a lot of these. We'll go over a couple of those in a minute, but these are prescriptive, debatable, and don't necessarily fit the team, codebase, or toolset you're working with.

There is a more general path: a process for improving code. It's called refactoring.

Refactoring

So refactoring has been around for about 25 years. The initial ideas were fairly specific to object-oriented code, and a lot of the literature kind of stopped there. I wrote a book detailing those techniques from a JavaScript perspective, and while promising, refactoring has limitations.

On the process side, things have never been better. You start with bad code, and slowly and safely make it better. Through widespread adoption of version control and unit testing, this process is safer and less error-prone than ever before. The basic idea is that with tests in place, you'll know if your code is broken, and can easily go back to a previous unbroken version. During that process, you can "improve" it. Initially, there were these ideas of "class constraints" and refactoring was pretty low-level. Now it's generally thought to include higher-level changes and also be backed up by tests. I'm personally of the belief that you can't refactor without tests, and encourage you to adopt that position.

And here's where we get into trouble. "Quality" is a hell of a thing to pin down, especially in JavaScript with so many paradigms and libraries and frameworks (oh my). So we've got two options, we either stick to someone else's ideas about what is good and bad code, or we make up our own... probably as a synthesis of books and code that we've read and written.

That is totally cool, but as a discovery engine for new types of quality, it is not great.

Engine 1: Personal Maintenance

So as a first engine of discovering what makes code bad or good, we can start with a simple question: do you want to maintain it in its current form?

If the answer is no, refactor away. This is a fine engine for personal code, but you might not be hitting your team's standards. So ok then...

Engine 2: Maintenance by Others

So now again, you have a simple question: would [insert teammate here] want to maintain it in its current form? If not, refactor away.

This is better, but still kind of myopic. We're writing code that people are going to want to work with more, but we're also doing something else.

We're making the codebase more survivable. We're giving it a chance to grow and reproduce. Maybe that's what the code wants to do.

If you're not buying that yet, it's cool. You'll still want to write code that your team wants to maintain, that gets more popular, and that people like to fork and build on, right?

That doesn't demand any will from the code itself. Ok fine. The next one doesn't depend on you believing any of the weird stuff, but it is new weird stuff.

Engine 3: Write code you want to run

Want to run? What's weird about that?

No, not like, press a key to make the code run. I mean, write code that you would want to execute personally... like, as the interpreter.

Ok. So here is some code:

arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
for(i=0;i==11;i++){
  arr[i] = arr[i] + 1;
};

Contrast that with this:

firstArray = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// assume addOne function
firstArray.forEach(element => addOne);

It might be weird to imagine yourself as the interpreter, and naturally, you're not going to account for all of the optimizations that could be made. But try to imagine if a boss told you to do something in the first way vs. the second. Here's what the first one says:

You have an array of 11 things, 0 through 10. Go through each of those eleven things, and assumed there's a number assigned to each of them. When you do that, call that number that's assigned to them i and every time you go to the next one change the number by adding one to it. And when you're adding one to that number and looking at the thing, you know the thing from the list. Ok, look at the number, you know i and pull out the number from the list, not i the number inside, and add one to that.

Is that written in the clearest way possible? No. But that's because you can read a for loop, which is obviously more clear. But anyways, what does the second one say?

You have an array of 11 things, 0 through 10. Give me a new variable with Add one to each one of the things in the list.

This is the difference between imperative and declarative coding. In the first, you specify the "how" and in the second, you specify the "what."

Depending on your team, your experiences, or the framework you're using, the second form isn't going to be obviously "better." However, considered as something you would rather be told to do, there's no competition. Granted, learning to use for loops might have been a battle hard fought for many coders, so dropping them might be tough without a good amount of time or concrete examples.

One more example of code:

thing = someFunctionThatReturnsABoolean();
if(thing){
  console.log("it was true");
} else {
  console.log("it was false");
}

vs

console.log(`it was 
${someFunctionThatReturnsABoolean()}`);

See if you can be the interpreter here... Ok. Here's my interpretation:

Store the return value of a function that returns a boolean in something called thing. After that, see if thing is true or false. If it's true, print "it was true" but if it wasn't true, print "it was false".

Vs.

Print "it was" and after "was" and a space, print the value returned by the boolean returning function.

Ok. These are fun, to me anyways. They might seem subtle or contrived, but try it a few times when you're trying to improve your code, and see if you don't come up with something you haven't thought of before.

You can learn these kind of techniques as a list of things to do and avoid, but this engine let's you discover them in a personal way. A few other quick examples:

  • I wouldn't want to have to remember that many variables
  • I wouldn't want to have to remember what variable inw was
  • I wouldn't want to do that many things at once

The list goes on. If you think of any software practices for "clean code" or "code quality" that can't be personalized like this, please let me know.

As a side note, I'd like to point out that tasks that are more loosely specified generally have more "dignity" in them, at least in the request. As humans, we generally like tasks with dignity, although I can think of one specific type of "work" where we don't: clicker games. We click, get get points, we click more. The thing about this though, although it's a bit of a stretch, MineCraft demonstrates this next point well. We don't stop at the undignified form of clicking. We build better pickaxes to dig faster. The undignified task is a means to an end. (Double side note, there is an interesting connection here to slow algorithms that we can use to search for faster algorithms, of lower complexity classes).

Our code in its undignified state is code we don't want to run, a dull pick axe. Refactoring is our tool to get a better pick axe.

Engine 4: Write code you'd like to meet (or be yourself)

This one really breaks with the traditional ideas of "quality," but puts some very important considerations into the same category as it.

Do you want to hang out with a grouchy person? Then don't write code that complains all the time.

What about a person that is overly picky and complicated? Then don't use tools and frameworks you don't know.

What about a person who takes foreeeeeeeeeeeever to do something? Bad performance.

What about someone who won't tell you what's wrong? Yep, that's code that fails silently.

So your code has a personality. Nope, that doesn't make it a person, but again, what does treating it like one cost you? Not much.

The good thing about this engine is that stops being about the structure of code. It starts being about what you value as a person, and creating programs that demonstrate those values.

From here, we can really explore what quality means. Is it code that ignores blind people through bad usability? Would you want to be that kind of person? Well, if you tell your code to do that, shouldn't you feel some responsibility?

If you make a Twitter Bot that's racist, how bad should you feel? If you were a parent or a friend, wouldn't you try to steer a person clear of this? Seems like a clear lack of guidance. The bot was racist, and was killed for it. Are the parents to blame?

A few passes of self as interpreter would have made this problem obvious. Run it for a week on your personal account, and see if it holds up to your values.

By the way, a lot of this experience based testing gets claimed by the design side of things, either "User Experience Engineers" or possibly just product signs off on it. Really though, there is a whole world of "non-functional testing" that programmers barely explore in comparison to unit/high-level tests of the "functionality" of the software.

These include:

  • Experience, including fun
  • Accessibility
  • Performance
  • Quality Assurance (kind of a grey area)
  • Security

But here's the other thing they include: Their business function. You know when people say, he's a real nice guy, but he is a hitman? Nah. Me neither.

But we do at times, get really focused on our code's organization and quality, along with our perspective on maintaining it, maybe with a team. While doing that, we can lose sight of the broader implications of quality: from making it work well for everyone, to making it safe, to making it fun, to making it a force for good in the world.

Another reason to consider why we should think of our code as more than just the lines on the screen is that there is a chance of it changing into policy or even law at some point. In actuality, there is a chance that for this particular task, it could be deautomated, and your program would be running by an actual person. This blends the concerns of Engines 3 and 4.

Recap

  • Code we want to maintain
  • Code others want to maintain
  • Code we want to run
  • Code we want to be

All it takes is admitting that computers are sentient beings, and the picture of computer quality becomes clear: It's not that different than qualities we want in a human. Through that process, our imagination for quality code is only limited by our imagination for behaviors and characters.

Make code that you want to live with in the world, even if you think it's just a dumb configuration of silicon and plastic... and it just thinks you're a dumb configuration of carbon and flesh.