One of the classic mistakes that beginning programmers make is this:
- Spend a long time writing code without ever trying to run it
- Finally run the code and observe that it doesn’t work
- Puzzle over the mass of code that’s been written, trying to imagine what might have gone wrong
This is a painful, slow, wasteful way to program. There’s a better way.
Feedback loops
Computer programs are complicated. Human memory and reasoning are limited and fallible. Even a program consisting of just a few lines of code can defy our expectations for how the program ought to behave.
Knowing how underpowered our brains are in the face of the mentally demanding work of programming, we can benefit from taking some measures to lessen the demand on our brains.
One thing we can do is work in feedback loops.
The idea of a feedback loop is to shift the mindset from “I trust my mind very much, and so I don’t need empirical data from the outside world” to “I trust my mind very little, and so I need to constantly check my assumptions against empirical data from the outside world”.
How to program in feedback loops
The smallest feedback loop can go something like this.
- Decide what you want to accomplish
- Devise a manual test you can perform to see if #1 is done
- Perform the test from step 2
- Write a line of code
- Repeat test from step 2 until it passes
- Repeat from step 1 with a new goal
Let’s go over these steps individually with an example.
Decide what you want to accomplish
Let’s say you’re writing a program that reads a list of inputs from a file and processes those inputs somehow.
You can’t make your first goal “get program working”. That’s too big and vague of a goal. Maybe a good first goal could be “read from the file and print the first line to the screen”.
Devise a manual test you can perform to see if #1 is done
For this step, you could put some code in a file and then run that file on the command line. More precisely, “run the file on the command line” would be the test.
Perform test from #2
In this step, you would perform the test that you devised in the previous step, in this case running the program on the command line.
Significantly, you want to perform this test before you’ve actually attempted to make the test pass. The reason is that you want to avoid false positives. If you write some code and then perform the test and the test passes, you can’t always know whether the test passed because your code works or if the test passed because you devised an invalid test that will pass for reasons other than the code working. The risk of false positives may seem like a remote one but it happens all the time, even to experienced programmers.
Write a line of code
The key word in this sentence is “a”, as in “one”. I personally never trust myself to write more than one or two lines of code at a time, even after 15+ years of programming.
Running a test after each line of code, even if you don’t expect each line to sensibly progress your program toward its goal, helps ensure that you’re not making egregious mistakes. For example, if you add a line that contains a syntax error, it’s good to catch the syntax error in that line before adding more lines.
To continue the example, this step, at least the first iteration of this step, might involve you writing a line that attempts to read from a file (but not print output yet).
Repeat test from #2 until passing
After writing a line of code, perform your test again. Your test might pass. Your test might fail in an unsurprising way. Or something unexpected might happen, like an error.
Once your test results are in, write another line of code and repeat the process until your test passes.
Repeat from step 1
Once your test passes, decide on a new goal and repeat the whole process.
Advanced feedback loops
Once you get comfortable coding in feedback loops, you can do something really exciting, which is to automate the testing step of the feedback loop. Not only can this make your coding faster but it can protect your entire application against regressions (when stuff that used to work stops working).
I consider automated testing a necessary skill for true professional-level programming, but it’s also a relatively “advanced” skill that you can ignore when you’re first getting started with programming.
Bigger feedback loops
In a well-run organization, programmers work in concentric circles of feedback loops. Lines of code are written in feedback loops that last seconds. Automated test suites provide feedback loops that last minutes. Individual tasks are designed for feedback loops that last hours or days. Large projects are broken into sub-projects that last days or weeks.
Takeaways
- No one’s brain is powerful enough to write a whole program at once without running it.
- Feedback loops shift your trust from your own fallible mind to hard empirical data.
- Follow the feedback loop instructions in the post to code faster, more easily and more enjoyably.
I love this
Excellent article Jason! Describing test-driven development (TDD) in terms of feedback loops without ever mentioning TDD. Whenever a developer fights TDD, it’s typically due to the absence of tight feedback loops in their development cadence. They’re just not mature enough with their development behaviors. Using unit test suites as that first tight feedback loop is a powerful tool for development.
Thanks! I realized after years of teaching testing that there’s maybe a certain prerequisite to understanding TDD that doesn’t have to involve talking about TDD, and is maybe easier to understand if all talk of TDD is left out.
I just wanted to point out that many developers use “TDD” without realizing it when they use LSP or other language processing in their editors.
In that case you end up with a “will the code compile” test that sometimes has immediate feedback as soon as you press a space bar, or a closing symbol (closing quote, closing parens, etc), and the editor does a quick syntax check and immediately marks your code if it has a typo or is missing the closing symbol.
This may not be considered as a step of TDD, but I often use dumb editors that don’t do language processing and sometimes write my first test something like “assert(1 == 1)” as a compile check that I can use as immediate feedback to test if my code compiles.
This concept ties in very closely to a related one: scope reduction. Breaking down a complex task also allows for one to think about how to minimize the scope.