Comment by dmitryminkovsky
4 years ago
Whenever this comes up, I think of this quote from The Practice of Programming by Brian W. Kernighan and Rob Pike [0]:
> As personal choice, we tend not to use debuggers beyond getting a stack trace or the value of a variable or two. One reason is that it is easy to get lost in details of complicated data structures and control flow; we find stepping through a program less productive than thinking harder and adding output statements and self-checking code at critical places. Clicking over statements takes longer than scanning the output of judiciously-placed displays. It takes less time to decide where to put print statements than to single-step to the critical section of code, even assuming we know where that is. More important, debugging statements stay with the program; debugging sessions are transient.
I found this lines up with my personal experience. I used to lean on interactive debuggers a lot, and still enjoy using them. They're fun and make for good exploring. But the act of figuring out where you want to print really makes you think in ways that interactive debugging cannot. I find the two forms really complement each other.
There's a part of me that wants to say that that opinion has to be taken with a grain of salt and a lump of paying attention to who is offering it.
Circa 1999, one would assume that Brian Kernighan and Rob Pike are largely drawing experience from working with C, which is a relatively verbose language. Single stepping through C code in a debugger is indeed a laborious process.
If you read accounts from Smalltalk developers, on the other hand, it's clear that they very nearly live their entire lives inside the debugger, and consider it to be an enormous productivity booster.
I would guess that there are several effects going on there. One would be that, in terms of how much effective work it accomplishes, a line of Smalltalk code is generally not equivalent to a line of C code. That has a big impact on just how many steps are involved in single-stepping your way through a region. The other is the nature of the debugger itself. Gdb and Smalltalk debuggers are wildly different pieces of software.
Linus used to be against kernel debugging for the longest time.
The core of his position (as I understand it) was that regularly needing a debugger is a sign that your software has "gotten away from you". You've let the software get to a state where it cannot easily be understood from the architecture and program text alone.
I do think debuggers can be useful when building up comprehension - particularly of other people's software.
It's very time consuming though, and at each "trace" you're only seeing one path through the program. Good architecture and documentation lets you understand all the possible paths at once.
I hold the very same opinion, interactive debugging in general should be a rare need.
If it's being used too often then it points to the fact that the software has to be run in order to understand it. It's representation is not sufficient to convey it's run time behaviour.
Also would like to point that dynamic languages in general require more debugging than statically typed one's since one can't be certain of the data flow within functions.
2 replies →
I’ve felt the same way about IDEs in general.
We need more tooling to help people understand and mitigate necessary complexity, not tools that help one muddle through or — I shudder to think — extend complexity.
I’ve changed my mind on this recently only because some IDEs have indeed become good at the latter.
Once you traipse into concurrency land, too, debuggers get much more tricky. You now have to consider the state of the thread you are in, plus all the others.
1 reply →
Agreed. The only time I've found a debugger useful is when the print statements aren't immediately giving clarity, and cognitive dissonance is settling in.
1 reply →
I guess you could view it that way although I prefer to think of what Smalltalk developers do as living inside a REPL. And that is indeed something I can relate to. I program Julia and I basically stay in a REPL all day. But I don't regard that as the same as using a debugger. Like a Smalltalk developer, I evaluate specific functions/methods with particular values. I don't step through code. I am pretty sure Smalltalk developers don't step through code a lot.
Rather they do live changes of their code, and then evaluate various objects to verify that things work as expected. That how I seem to remember working in Smalltalk many years ago.
I am not a fan of debuggers, although I do use the REPL a lot. I suppose like Rob Pike, I used them only for very limited tasks, such as getting a stack trace or getting some sense of control flow. But as soon as I have that, I spend more time looking at code and reasoning about it, than stepping in a debugger. With a REPL I can try out assumptions I make about the code, rather than being forced to step through it.
Perhaps it has to do with dynamism? I find myself "debugging" way more in dynamic languages that emphasize interactive development. At that point, the line between running code and debugging gets pretty blurry, since "stopping" execution at some arbitrary place is not very different from normal development anyway.
I think this is a really good split in methodology to identify. I've noticed the same in the way I debug static vs dynamic languages. It seems to reflect the nature of the language; dynamic languages are powerful because they are fuzzy, but that comes at the cost of comprehension, and static languages tend to be the opposite.
It also matters which debugger you are using and which features it has. I personally find gdb to be completely awful, while some IDE debuggers are a delightful. With Java, you can hotswap code into a running program. And you could write the majority of your program like that. It becomes an interactive experience a little bit similar to using a jupyter notebook.
You can also make a weird sort of UI this way, where the way you interact with your program is by changing the code / and or state while it's running. Breakpoints prompt for input.
> we find stepping through a program less productive than thinking harder and adding output statements and self-checking code at critical places.
Uh, me too. That's why I don't single-step through huge chunks of a program.
I use code breakpoints.
I'm getting the impression a lot of people don't know basic debugger features like "continue" or setting breakpoints while paused at a breakpoint.
In a way, the print-debugging is a sign of 'owning' the code, when a developer is very much familiar with the structure and internal works of the project. This is akin to surgeon precisely pointing the scope and scalpel.
Add to this a need to build a debugging-enabled version of the project - an often long-running process, compared to a few edits to some 'release' build.
On the other hand, when dealing with an unfamiliar or complex and well-forgotten project, debuggers become that discovery and exploration tool that offers a wider context and potentially better situational awareness.
Of course, mix-in some concurrency and either debugging approach can equally become cumbersome without proper understanding of the project.
I still resort to gdb, but mostly when I need memory access breakpoints for when someone (occasionally me) stomped on memory.
I think the Key phrase from that quotation is "thinking harder". A debugger gives you all of the programme's state as a kind of vast animation, so it's easy to start working with one thinking "Something's going wrong here, so I'll just step through the whole programmme and see when things start looking like they're going wrong". It's then easy to miss the problem due to the vast quantity of data you have to parse. Using print statements, in contrast, forces you to formulate simple hypotheses and then verify or falsify them, e.g. "I think this variable in this function is causing the problem" or "I think everything's fine with the execution up to this point". I.e. the very fact that a debugger works so well at giving you an insight into how the programmes state can itself be part of the problem: it can be overwhelming.
Why would one step through the whole program? Use the approach you describe for print statements, but for breakpoints instead. Set them, inspect the relevant state when one is hit, then resume execution until the next is hit.
Why would I do this manually using some fiddly UI instead of automating it using the programming language I already have at my fingertips?
Using the programming language itself, I can extract exactly the information I need to see, transform it into exactly the shape that's easiest for me to inspect and combine output from different places in the code to create a compact list of state changes that's easy for my eyes to scan.
What I have found is that my debugging problems are either too simple to require anything more than thinking or too data dependent for a debugger to be the best tool for the job.
The Chrome devtools make adding a logpoint as easy as setting a breakpoint. So instead of adding your print statement, and rebuilding the app you can do it live. I think this is strictly better as you still get to enjoy the mental exercise of deciding where to put the logpoint. Even better, you can run anything you want in a logpoint, my favorite is console.profile/profileEnd() to get a cpu profile between two lines of code.
There's another methodology that Brian and Rob Pike miss.
Rather then stepping through a program. Add breakpoints and just step from breakpoint to breakpoint.
With a good IDE, adding a breakpoint and hitting a shortcut key is faster than a print statement and on the GUI IDE your debugging sessions are not transient.
The only time their advice makes sense to me is when I'm in an environment without a gui. Even then jetbrains has a "ssh full remote mode." However at my company I have found that this feature doesn't work under Nix (nix-shell) so we all just use print statements.