IEEE Software - The Pragmatic Designer: Code is Your Partner in Thought

To be published in IEEE Software in 2020.

ABSTRACT: To understand technical debt, it’s necessary to see a program as having characteristics of both a machine and of pure thought. Technical debt arises when a program works fine as a machine but expresses my thoughts poorly. Debt in a program mentally impairs the programmers working on it. I can remove that impairment by refactoring or rewriting my program so that it again expresses my thoughts. Source code carries our thoughts with a directness and immediacy that is not possible with steel or concrete. It enables a cognitive coupling between programmers and their code, a kind of extended cognition, offloading mental burdens into the code and allowing us to produce far more complicated programs.


Writing about the buildup of technical debt, Ward Cunningham said “Although immature code may work fine and be completely acceptable to the customer, excess quantities will make a program unmasterable, leading to extreme specialization of programmers and finally an inflexible product.” [1] In this article, I’ll dig into the apparent paradox: how can code work fine and yet be unmasterable?

Our code has characteristics of both a machine and pure thought. Technical debt arises when these do not line up. Software developers have a hard time seeing how weird this is because we’re too familiar with our subject. To help us see it from a non-programmer perspective, I’m going to try something a bit different in this column. What follows is a fictional conversation between me and a non-programmer friend.

Explaining technical debt to a friend

Friend: I read your description of what you called ur-technical debt in your last article. [2] You say that technical debt happens when your ideas diverge from the program you are writing. I don’t understand how that can happen. You are the one writing the program, so how can your ideas be different than what you wrote?

Me: I suppose I could deliberately write something that doesn’t agree with my thoughts. I’m reminded of that Star Trek episode where they confuse a computer by paradoxically saying “I’m lying.” Tech debt isn’t like that. Instead, it’s that my thoughts change over time, which means that I could look at older code and have a newer thought in my head.

I’m familiar with thoughts evolving. I could write an essay that expresses my thoughts, put it on the shelf, then return to it later and discover that my thoughts have changed. Are programs like essays then? They seem quite different. I can’t run an essay the way I can run a program. If my thoughts change, I consider my old essay to be wrong. If your thoughts change, does that make your program wrong?

Sometimes, but that’s not what technical debt is about. Technical debt happens even when the program I wrote continues to work just fine.

How can it work fine if it doesn’t match your thoughts? It seems like you are saying it’s both OK and not OK at the same time.

I’d not looked at it that way before, but I see the contradiction now. Let me try an example. Let’s say I built a program to control a traffic light near a school. In the mornings and evenings when the school is opening and closing, the traffic light has a turn arrow to let traffic into and out of the school.

The traffic light has software inside of it?

Right. And let’s say the traffic light was installed and working OK, as I said earlier. But then someone tells me that school is closed in summer, in which case we should disable the turn arrow.

So that’s an example of your thoughts changing but the machine working just fine? It seems like it isn’t working fine since you now know it should behave differently.

This is what Ward Cunningham was talking about with the comparison between waterfall and iterative processes. [1] In a waterfall process, I would have looked at all the requirements and built a traffic light that did everything that was asked. That seems sensible, but he argues that in practice it’s better and faster to use iterations. I used iterations, and considered only one requirement, built the traffic light, then considered the next requirement.

This is making more sense now. The traffic light worked just fine considering just that one requirement. When you looked at the next requirement, your thoughts changed, the traffic light software stayed the same, and technical debt appeared.

Right. It’s not just requirements though. It could be something I learned about my solution or other code I’m depending on – anything I learn that changes how I’d design the code.

Head versus code

Let me see if I can simplify this a bit. Let’s use your example but change it slightly, so that you never design or build the traffic light. It seems like the technical debt happens when you read that second requirement and your thoughts change. We can ignore the code in the traffic light. That is just a distraction.

Ah no, the code is critical. If I read one requirement, get my thoughts straight, then read a second requirement, causing my thoughts to change, that’s not technical debt. Let me go back to your comparison to an essay. The code is like an essay in that it expresses my thoughts. Technical debt happens not just when my thoughts change inside my head, but when there’s evidence of my thoughts outside my head that hasn’t caught up yet.

That is indeed different. So I must consider both thoughts in my head, which change over time, as well as code out in the world, which also changes over time, but not in lockstep. When the two aren’t aligned, that’s technical debt.

Yes, you’ve got it.

I’m not a programmer myself, so I admit that when you talked about a traffic light I was imagining some old fashioned mechanical traffic light. Clearly a computer is a machine like that traffic light, designed by engineers and put into the world to do useful things. And much of what you’ve said makes sense for a physical machine, including which process I follow (e.g., waterfall or iterative) and the ideas in my head changing over time as I learn about new requirements – all of that would apply to a mechanical traffic light. When you said your program is like an essay, however, I’m lost because I can’t see how a mechanical traffic light is like an essay.

The core of any computer is symbol manipulation, usually things like adding numbers or moving data around. In our requirements, we also use symbols, but different ones, such as traffic, turns, and lights. In the decades that people have been writing software, we’ve created programming languages that let us use symbols that are closer to how we think about the problem and solution. In my code, regardless of the programming language I’m using, I reveal the symbols from the requirements, and of course symbols related to my solution.

So you are handed the symbols from the requirements and you invent the symbols in your solution?

Not quite. The requirements are never fully clear and I must work to clean them up before I can write code. Nor is my solution ever completely novel: I’m always borrowing ideas from existing solutions.

Programming languages do a pretty good job of expressing my thoughts about the problem and solution, but they never express everything I’m thinking, though who knows exactly what form my thoughts take in my head. I have to work to express my thoughts as a program, and I’m human, so I make mistakes in my thinking and in the translation. We call those bugs.

When I write an essay, I also have to work. Often the act of writing forces me to clean up my thoughts.

Programming is like that too. One of the reasons I use an iterative process is that by writing code early, based on incomplete requirements, I’m forced to clean up my thoughts before I go too far.

This cleanup we’re talking about just now – that’s between the times when we first think our thoughts and when we first express them. If I understand you, though, technical debt happens after the first expression. So it’s different from the idea that putting pen to paper helps us clean up our thoughts.

Yes, technical debt is about what happens after I’ve put my best thoughts on paper, or into a program, and then those thoughts change.

Mental burdens

Let’s go back to your example. How would you modify your program to handle the second requirement, the “except in summer” part?

I have a choice to make. The easiest thing to do would be to find a place in the existing program to add a condition, so instead of always using the turn arrow, the program would do the equivalent of “if it’s summer, don’t use the turn arrow.”

That sounds straightforward. Why not do that?

This is a pretty simple example, so maybe I would. But you’ve probably heard people say about legal contracts, “the big print giveth and the small print taketh away.” It’s easy to be reading a program and get one idea, but then read further and find out that’s not right. For example, I could have named a part of the code “enable the turn arrow” and inside that code put the conditional that, in fact, doesn’t enable the arrow in summer. That would confuse my teammates. Heck, after a few months, it would confuse my future self.

From your description of legal contracts, you must know my mobile phone company. My contract is fifty pages long and I’m sure I’ve signed away my firstborn somewhere in it.

Fifty pages is a pretty small program. The mental load on programmers is significant. They must keep clear in their heads not only the problem they are solving but also the solution, all in the fussy detail that computers demand. Imagine your contract a thousand times as long, but worse because everyone on your team changes it every day.

So the technical debt is when you choose the easy thing, the confusing thing?

Many people use the term that way. They point at code they don’t like and say “that is technical debt.” In Ward Cunningham’s original description, however, he compares the debt to the choice to write code despite only having a partial understanding of the problem and solution. [1] A partial understanding gets you started on the project faster, like when you borrow money to buy a house.

I like the way the debt metaphor highlights the interest payments. [2] By writing easy-but-confusing code, I’m putting a mental burden on my teammates and myself, a burden that’s like an interest payment. The more confusing the code is, the higher our interest payments on the debt.

I hope you’re going to tell me about your other choice, the one that avoids you going to a shady lender who will break your legs if you fall behind on payments.

Aww, you care! Yes, I can rewrite parts of the program so it lines up better with my thoughts. That reduces the mental burden on me and on the rest of the team.

Partner in thought

The more you tell me about programming, the stranger it seems. It’s easy to see a computer and its software as a machine. What you’re talking about is quite different: In the act of programming, you are thinking thoughts and expressing them in the code while the problem and solution evolve in your mind.

Once you are a programmer, this seems normal, and you forget what it was like to be an outsider. I would guess that outsiders see programming as some kind of meticulous activity similar to writing cookbook recipes. As an insider, I agree that it’s meticulous, but what’s more important is the cognitive interaction between me and my code. The design is always changing, and I’m always reasoning through the implications of those changes.

In that turn signal example, you mentioned a choice, and said that you could write a program that works, but that would confuse your coworkers. To reason through your program, does it matter what form the program itself takes?

If I were to reason like a CPU, the form of the program would be irrelevant, but that kind of reasoning is tedious and error-prone. I would risk losing sight of the forest for the trees. It robs me of the symbols that I put into the program specifically to help me reason. In fact, we have things called code obfuscators that take code that’s easy to understand and turn it into gibberish that behaves identically. Those are commonly used when delivering JavaScript to a browser, so that the code still works as a machine but is hard for other programmers to reason about.

The form of the program is critical if you want a team of programmers to understand and evolve it. Just imagine if I took one of your essays, as you were writing it, and everywhere you’d written “person” I replaced it with “lawnmower.” As you read over the essay to continue editing it, you could mentally translate, but it takes effort. How many more word swaps would it take before I overwhelmed your ability to adapt?

You are a mean person. I will change the password on my computer before you visit next time.

Other than mischief, nobody wants to contort your essay. Software is different. It costs time and money to go back and align the program with the thoughts of the developers. There’s a big incentive to ask the team to accept the mental burden of translating between their current best understanding, which lives in their heads, and the code as it already exists. What’s worse, I don’t know a good way to quantify that mental burden, so I have a hard time persuading people to pay down debt when they instead want me to build the next feature.

When you put it this way, I’m realizing how dependent I am on seeing the text of my essay while I’m writing it. It’s not as if I compose the whole thing in my head and then type it in as fast as possible. My ability to think clearly is connected in a tight loop with the in-progress essay I see in my word processor – a kind of extended cognition. [3]

Same here. My code is my partner in thought. My ability to do my job is dependent on seeing my thoughts as symbols in my program. My head simply isn’t big enough to hold all the details, so I push my thoughts into the code and rely upon my ability to read the program to recover those thoughts and to revise them. Technical debt damages or breaks that partnership between my mind and the code.

I agree with you that programming is stranger than it appears. It has a dual nature, both machine and thought. Like other engineers, programmers create useful machines: the computer plus software. But source code carries our thoughts with a directness and immediacy that is not possible with steel or concrete. That sets up a cognitive coupling between programmers and their code, a kind of extended cognition, offloading mental burdens into the code and allowing us to produce far more complicated programs. That is, so long as we keep our tech debt low enough.

This conversation has been a winding road, but I now see why a program can work perfectly fine as a machine but be unacceptable to its programmers.

References

  1. Ward Cunningham, The WyCash Portfolio Management System, OOPSLA 92, Vancouver, British Columbia, Canada, Addendum to the Proceedings, Experience Report, 5 - 10 October 1992, https://c2.com/doc/oopsla92.html.
  2. George Fairbanks, Ur-Technical Debt, IEEE Software, Vol 32 number 4. July/August 2020, Draft.
  3. Andy Clark, David J. Chalmers, The Extended Mind. Analysis. 58 (1): 7–19. doi: 10.1093/analys/58.1.7, January 1998.