I've been thinking about debugging lately. In my humble opinion, there are four phases in the art of debugging.
The first phase, which I think of as the debug prevention phase is where you proactively do something to prevent silly bugs from happening.
The thing is that debugging is time-consuming. Not only because you have to find the bugs, but also because it disturbs the flow. Silly bugs are like faulty brakes on a bicycle. It's neither fun or productive to push and push to get up to speed just to find yourself hanging over the handlebars at a sudden halt.
So it's worthwhile to do something to avoid obvious sources of bugs.
Here's an example of an obvious source that I recently encountered (apologies to the source): you have a model class with a certain state which can change on its own while also being changed in the user interface by the user. One way to approach this is to code the model and user interface separately, each with its own state, then add a bit of glue code to synchronize the two.
The (obvious) problem is that the two copies of the states can get out of sync, in which case you're toasted. It's also pretty confusing - when you ask for the real state, the master state so to speak, where do you ask? The model or the UI? So avoid the copy in the UI, even if it's less convenient to have to go ask the model all the time.
Of course, obvious sources of bugs are probably not obvious unless you spend some time reflecting on what you're doing. Here's a simple rule of thumb: If something is so confusing to think about that you can't immediately make head or tails of it, then it's probably worthwhile spending some time refactoring it because otherwise it's likely to become a source of bugs. Sometimes it's just a matter of renaming variables and functions.
The second phase is testing. This is where you find the bugs. The relation to debugging is that you should test and never assume that things are working just because the compiler/interpreter/whatever ate it. Untested code is an obvious source of bugs and it's always better to fix the bugs before you've embarassed yourself by shipping them. Debugging in the open is not fun. It's also much easier to find the bugs if you test early.
The third phase is the trial-and-error phase. This is the initial reaction most people have when something stops working: What, it worked before, let me just try this, then it'll probably work again. Repeat.
What's important in this phase is to leave it early if the first couple of tries don't work out. Trial and error is time-consuming and mentally draining because it puts you through lots of frustrating failures with no clear reward. Don't get stuck.
Instead consider proceeding to phase four which is the observation-thinking cycle. Here the goal is not to fix the problem, but to understand what's going on. Think of yourself as the analyzing surgeon who's on a mission to make the smallest possible cut, not the rampant safari hunter with the over-size elephant gun.
How do you understand your program? By running it, either in a debugger or with print statements sprinkled so you can see why the code is failing. You start at the innermost level, then gradually trace your way through the call stack.
The idea is not to trust anything you see. It's always a bad idea to assume something is working before you have printed the result and verified it in the actual faulty run. For instance, it might be a good idea to really start at the innermost level even though there's nothing exciting going on there. I've spent countless hours trying to understand why something obviously correct wasn't working just to find out that the code I was looking at wasn't actually the code running. Maybe you've forgotten to save the file. Or upload it. Or recompile. Or maybe it's running another, similar function you've forgotten about. Or maybe it suddenly crashed because of something seemingly unrelated. A print statement can tell you the truth.
You observe (real output, not source code) and you think, add more debug code, and observe and think. When you're done, you know what the program, your little creation, is doing. That's a nice, rewarding feeling. You're in control. And it makes the kind of story you can tell your (geeky) friends: I had this really annoying bug, and I traced it through ..., and finally I had it nailed!
Sometimes it's difficult to proceed to phase four because the debugging environment, frankly, is not up to the task. In which case you'd better start working on the debugging environment rather than building up frustration when the bugs begin to manifest themselves. So little is needed - if you can get printf-like output and short compile-run cycles, then you're good to go.
Nice article, you described debugging beautifully. I especially liked the last part, about "phase four". I agree that printf debugging is really all you need - anything else is maybe useful and more productive, but ultimately not essential.
ReplyDelete