Hacker News new | ask | show | jobs
by maximus1983 2600 days ago
> Doing “something else” means (1) rethinking your code so that it is easier to maintain or less buggy (2) adding smarter tests so that, in the future, bugs are readily identified effortlessly. Investing your time in this manner makes your code better in a lasting manner… whereas debugging your code line-by-line fixes one tiny problem without improving your process or your future diagnostics.

I've been a professional now for about 15 years and very, very rarely do I get to work on "my code". Almost all of the code I have to work with was written by someone else originally and I have to just modify the system for new requirements. Tests do not exist or if they do they are largely incomplete.

So the only thing to do is to step through find the problem, fix the ticket and move on.

Sure If I get to design the system I normally write it very simply / well structured with appropirate levels of abstraction and with enough tests to expose the bugs in my code. But very rarely do I get paid to work on my code, because my code doesn't need a lot of maintenance. I normally am asked to make changes to bad systems.

> Brian W. Kernighan and Rob Pike wrote that stepping through a program less productive than thinking harder and adding output statements and self-checking code at critical places. Kernighan once wrote that the most effective debugging tool is still careful thought, coupled with judiciously placed print statements.

Thinking harder when I have a project with millions of lines of code (this is normal in large financial systems) won't help me. A debugger will.

I think a lot of these famous programmers have never had to work with something terrible and probably never will and that is why they make such blasé statements.

3 comments

I find this kind of thinking to be borderline insanity. Why would I expend so much mental energy trying to understand what the past dozen developers were thinking when it's trivial to just inspect values and deal with the reality of the system as it is right now. If a function is expected to return a value of 25 and it's returning 17, I don't need to reason about their code to fix it. My tactic is always to write some very tight unit tests around the sections of code closest to the problem and run them through a debugger so I can see where it goes awry.
I don't think what you are doing is vastly different than what is described. "Thinking hard" is about trying to isolate the problem. RMS famously said that you should try to only debug the code that is broken rather than trying to debug the code that isn't broken (this is especially true in big systems). Where is the error happening? How are these things connected? Etc, etc. When you write very tight unit tests around the sections of the code, you have to do exactly the same thing (and I would argue that writing those tests is a good tool for "getting into" the code).

Next: running a debugger. I use print statements, but seriously, what's the difference? You use watch points in the debugger. Same thing. Once I have tests I find it easier to run them and look at the output of my prints as opposed to stepping through the code. Stepping through the code requires you to remember what you have done before and what the output was (granted debuggers that allow you to go backwards are helpful). If you can do that, then it's all good, but I find that it's easier for me to essentially create a log and read through it. It's just a bit more structured, but in the end it's exactly the same thing.

I think the biggest mistake that less experienced programmers do is that they don't try to reason about the code before they start. It's that isolation that's key, not how you are displaying the state of the program. Single stepping is fine when you've got 100 lines of code, but when you have thousands or millions of lines of code, it's not going to work. You need to be able to work your way backward from the error, reasoning using the source code as a map. You then use debugging tools (or printfs) to narrow down your options.

> I use print statements, but seriously, what's the difference? You use watch points in the debugger. Same thing. Once I have tests I find it easier to run them and look at the output of my prints as opposed to stepping through the code.

Maybe I have been spoiled by Visual Studio but as you said there are plenty of options in the debugger for winding back execution, changing execution, inspecting Objects, inspecting the state of the stack at the time, I can debug other people's assemblies, I can debug machines remotely.

Writing out to the console / log is my last resort.

> Stepping through the code requires you to remember what you have done before and what the output was (granted debuggers that allow you to go backwards are helpful).

No it doesn't require you to remember what you have done before. Even relatively basic debuggers such as the JavaScript debuggers in most browsers have a stack trace with what has been called where.

>If you can do that, then it's all good, but I find that it's easier for me to essentially create a log and read through it. It's just a bit more structured, but in the end it's exactly the same thing.

I don't understand why you would claim an inferior tool is better when a far superior one is available. It would like saying that a Impact Wrench / Spanner and a Spanner are the same thing, technically they both undo bolts, however one makes it far easier than the other.

> "Thinking hard" is about trying to isolate the problem. RMS famously said that you should try to only debug the code that is broken rather than trying to debug the code that isn't broken (this is especially true in big systems). Where is the error happening? How are these things connected? Etc, etc. When you write very tight unit tests around the sections of the code, you have to do exactly the same thing (and I would argue that writing those tests is a good tool for "getting into" the code).

Except it doesn't help you find the code. With a debugger I can stick in some breakpoints where I think the code is going to get hit and then walk myself back from there on what is called and in what order. I work with some terrible systems where it isn't obvious how the code is even executed because "senior" developers have abused IoC / DI, Reflection and Delegates in such a way to obscure what it is actually doing.

As for writing Unit-tests around sections of code. In quite a few areas I have been barred from doing it (for political reasons) and other times you realistically can't because your test setup would be just too complicated.

> Single stepping is fine when you've got 100 lines of code, but when you have thousands or millions of lines of code, it's not going to work

Yes it does work. I did it on Friday. I stick a break point on the entry point (in this case an ASP.NET MVC controller) and F10 and F11 my way down through the stack. I learned more doing this for 30 minutes then I did the previous 2 hours of looking at how the project was structured.

This "think harder" is akin to telling a man to dig harder with a shovel when they have a JCB excavator sitting round the corner.

This is my experience as well. Debuggers are extremely helpful when trying to integrate third party code with incomplete documentation. It's one thing to run a print statement at a specific level, but having the ability to explore an object and it's neighbouring objects can immediately provide insight instead of trying to find the function in the source code and deciphering it.
This is how the system ends up as a maintainable mess in the first place, where the fix for the aforementioned bug was to +8 and move on, which causes (at a later date) some other section to break because instead of 25, it's returning 33 so the developer adds a -8 and moves on...
I assume you meant an unmaintainable mess, and not a maintainable mess, but nevertheless, debuggers are not the cause.

You get messy code at least as quickly without a debugger, possibly even faster since bug fixes in my experience tend to be even more shallow, in many teams.

The values of the management, the values of the team, and resource availability is what matters in regard to writing, and keeping code maintainable.

I've worked with people who have had a similar idea, that using debuggers are somehow "bad", I have also had to rescue them by using a debugger to understand issues with code they had struggled to figure out for days, or even weeks, meanwhile causing severe issues for clients.

When you claim tools as the cause for your mistakes, then it could certainly be the case that the tool is of bad quality, which clearly is not the claimed issue with debuggers in the article. More likely though is that you haven't really learned when, and how, and why to use the tool.

Debuggers are brilliant for validating if you mental model of the code is correct, or more commonly, to understand how it's wrong. This can have a side effect of solving bugs, especially bugs in your own thinking

"Maintainable mess" is the apotheosis of every human endeavor.
I think it's a fantasy that if it were only "my code" it would be perfect and simple. Most of the worst code I've worked on was my code, after it had grown and adapted to new requirements and after I'd forgotten why I did things the way I did them. Tests are necessary but far from sufficient for making a system comprehensible.
Sure I’ve written some awful code in the past but I am at the point that when building certain application types I know exactly what I am doing.

If I was writing code in c++ I would probably create something awful because I wouldn’t know what I would be doing.

> I think a lot of these famous programmers have never had to work with something terrible and probably never will and that is why they make such blasé statements.

Do you think they never had to work with something terrible, or when faced with something terrible, they took the time to make it not terrible? The track record for the software these two have built speaks for itself.

I think their definition of terrible and mine are very different.