Hacker News new | ask | show | jobs
by Stratoscope 2604 days ago
I practically live in the debugger. I do use it sometimes to debug a problem, but most of the time I use a debugger to avoid debugging. I use it for coding.

I spend a lot of time working with APIs and libraries that are poorly documented and often that I haven't used before.

Instead of writing out a bunch of code based on my limited understanding of the docs, and likely with many bugs, what works for me is to just write a few lines of code, until I get to the first API call I'm not sure about or am just curious about. I add a dummy statement like "x = 1" on the next line and set a breakpoint there.

Then I start the debugger (which conveniently is also my code editor) and hopefully it hits the breakpoint. Now I get to see what that library call really did, with all the data in front of me. Then I'm ready to write the next few lines of code, with another dummy breakpoint statement after that.

Each step along the way, I get to verify if my assumptions are correct. I get to write code with actual data in front of me instead of hoping I understood it correctly.

If I'm writing Python code in one of the IntelliJ family of IDEs, I can also hit Alt+Shift+P to open a REPL in the context of my breakpoint.

Of course this won't work for every kind of code. If I were writing an OS kernel I might use different techniques. But when the work I'm doing lends itself to coding in the debugger, it saves me a lot of time and makes coding more fun.

5 comments

Where you would set a breakpoint at x=1, I would insert a syslog statement and dump the data into the log file. Since syslog is another process I can watch the behavior of the program while it is in operation alongside the other programs on the system. I've actually only used debuggers on systems which don't have any logging infrastructure.

At my last job for example, I frequently would plug the JTAG in to check the instruction pointer and read from the memory-mapped flash on our safety MCU because there was no other way to read data off the device after a crash.

And it was also an asset in quickly testing the programs I wrote for TI's N2HET because I could pause execution after the programs were loaded into the HET instruction ram, use the debugger to configure the variables in the instruction RAM and set the HET executing and watch the output on my oscilloscope. This ability to manipulate memory in the running program is very useful.

I used it very frequently when testing new components on our system buses because I could halt execution and configure DMA transfers and also inject data into system ram. So I could quickly validate my understanding of the reference manuals.

I think both approaches have merits and if someone tells you that one is superior to the other, that says a lot about their level of experience and the types of work they've done.

Those are some great debugging stories. Yep, for that kind of work I would probably do something more like what you were doing. I also appreciated your last comment:

> I think both approaches have merits and if someone tells you that one is superior to the other, that says a lot about their level of experience and the types of work they've done.

Indeed. We have so many great tools available to us, but the best choice of tools will vary a lot depending on the work you're doing, whether it is logging, JTAG and a 'scope, or whatever you have available.

It's quite a contrast to this quote from the article:

> ...the fact that Linus Torvalds, who is in charge of a critical piece of our infrastructure made of 15 million lines of code (the Linux kernel), does not use a debugger tells us something about debuggers

I don't think that tells us anything about debuggers, it just tells us that they aren't useful for Linus's particular work, or possibly he just doesn't like them. I doubt that Linus has ever said, "You should never use a debugger regardless of the kind of programming you're doing."

I have this same experience in python.

In Java, I did this a lot less because I could rely on the type of the interface to know what I was going to get back from the function call (with the caveat it might be null). I knew exactly what the type of the arguments needed to be.

You generally don't have that in python. Without extensive documentation (if you are lucky some newer code may make use of type annotations) you just have no idea with some interfaces what needs to be passed and what you will receive. Even with extensive documentation, the majority of well documented libraries are all documented in a different way and getting the same answers that the java method signature gives can be a confusing experience.

The tendency of functions to mask the signature of the method they simplify a call to by simply declaring args, *kwargs also makes it a bit annoying to discover the true type signature outside of a debugger.

Also, you get back a dict or a tuple... but what is in it?

The most productive experiences I've had with debuggers is writing the code as it's running. Relating your example, I would break just after some api call, with all the state there and then write the next lines of code, then run them, then write a few more and so on. So much faster than read log, make changes, recompile and rerun from scratch workflow.
If you were using a language with stronger types you could avoid the whole manual process you describe and rely on the compiler to do everything the debugger is doing for you now.
When you live in the debugger, you understand your code by seeing how your code work. When you live without the debugger, you have to imagine how your code work. The speed of seeing how your code work cannot match the speed of imagining how your code work. In addition, see your code works brings a lot of noise that is not part of your focus; in contrast, you only imagine where you focusing. Of course, the effectiveness of each depends on your experience in each respectively.

There is no denying that seeing is the real world, and imagining is just imagination :).

Being able to visualize your code in your head is a great advantage, and I've fixed many bugs that way. But sometimes your imagination hits its limits and using a debugger to understand what's happening in the code is very useful.

Some cases where your imagination could be limited are:

- Code that you didn't write.

- Code that you wrote long enough ago that you don't remember the details.

- Code that you know very well, but you're having a bad day and can't figure out the problem just by thinking about it.

> Some cases where your imagination could be limited are:

> - Code that you didn't write.

This has become the norm with software written by a team; and to my stress, I find many coders have given up understanding code that other people wrote and reduce to the minimum that get by. My premise is the necessity of understand the code -- not only how the code work, but also how the code is conceived and where the code is evolving toward. After that premise, there is no difference between code that you didn't write or you did.

> - Code that you wrote long enough ago that you don't remember the details.

That says a lot that the code was not written in its optimum way. Treat that as a bug, and debug why the details cannot be easily retrieved.

> - Code that you know very well, but you're having a bad day and can't figure out the problem just by thinking about it.

You should always take a rest and tackle it the next hour or next day when you can work effectively. Continue to push through a bad day only has the opportunity to make the day worse.

That says a lot that the code was not written in its optimum way. Treat that as a bug, and debug why the details cannot be easily retrieved.

I wrote code dealing with railroad car repairs years ago.

https://www.railinc.com/rportal/documents/18/260737/CRB_Proc...

Should I be able to remember all 200+ pages of the audit rules by heart for both the car owner side and the repair shop side and remember why I wrote all of the special cases?

If you are writing special cases because a 200+ page rulebook says you need them, then the special cases should be commented to indicate exactly what rule they are meant to address such that someone with the rulebook can quickly look it up.

Ideally, you would have all the rules encoded in a central place so this lookup becomes obvious from the structure of the code, but that is often not possible.

I am involved in a simmilar project now. We have a proprietary data format which has, over the years, evolved slightly different versions as different teams extended it in slightly different and incompatable ways. The code has all kinds of special cases, to the point where we developed internal style guidelines for how to comment them

So now you have the rule book and the comments and if there is a bug in any of the code, an I suppose to remember what all the code does and think through it? Am I suppose to be able to just think through why the one set of hundreds of records that came from one of 200+ repair yards is giving back erroneous results or should I just use a debugger and set a conditional breakpoint?
Even when I’m starting a program from scratch It’s not just my code I have to understand, it’s all of the libraries, franeworks, APIs, etc. I’m integrating with.

For instance, am I suppose to “imagine” how all of AWS Boto3 functions work?

https://boto3.amazonaws.com/v1/documentation/api/latest/inde...

What reason is there to think “Imagining how your code works” should always be faster than “seeing how your code works”? It is certainly the case that sometimes one of these is faster than the other but it’s gonna be codebase, tooling, and scenario dependent which one actually is faster.
Did I say "always"? If I did, that is not the point.
I’m trying to think through why removing the debugger should help one build a mental model of the code faster than having constant access to one.

I guess I can imagine workflows that are overly debugger dependent - the programmer has a “trust nothing” mentality and checks the behavior of the code against their mental model of the code “too much” thus slowing them down.

But I think theres reason to think the programmer can maintain a mental model of the program while using debugger queries to verify aspects of that model in a way that’s a lot more efficient than relying on mental model alone. Beyond that at some point observation of the behavior of the program will come into play — and I don’t see why utilizing the power of a debugger to generate a lot of precise observations quickly isn’t a pure win ...

Seems that you are agreeing with imagining and use debugger to verify is a lot more efficient. And for verification, print is more direct than orchestrating the breakpoints and then print with debugger's syntax (or gui navigation).
I see imagination as a processor's L3 cache. It's really fast and ideally you want everything to be in there.

But it can only fit so much so I prioritize hotpaths to live in my imagination.