I would go so far as to suggest that if you need a debugger, you're probably doing it wrong.
I have found that code that needs a debugger is code that is written in an extremely stateful fashion.
The very reason why debuggers exist is to deal with code that has a dozen variables book-keeping complex state that you need to track closely to figure out how something works (or why something doesn't). Debuggers even have "watches" to track something closely so that it doesn't change underneath you, when you aren't looking.
To me this is a code smell. I would suggest that instead of using a debugger, we ought to be composing small functions together.
Small and pure functions only need an REPL to test.
Once you make sure the function is behaving right, you can almost just copy paste over the test cases you ran in your REPL into unit tests.
The simplest way to enforce this discipline is to do away with assignments, and instead constrain oneself to using expressions. This forces one to break code into smaller functions. Code written this way turns out to be much more readable, as well as reusable.
I suggest that we eschew the debugger in favour of the REPL.
This is perhaps one of the most harmful statements I’ve ever read on Hacker News. You’re not doing it wrong if you use a tool to help you understand what your program is doing. Should you follow good design patterns, like using small functions when writing your software? Yes. Should you minimize complex state? Sure. Should you use tools to help you understand what your program is doing? Absolutely. The two are not mutually exclusive.
There are a LOT of different ways to write software. Some more stateful than others. Some lower level than others. Some more asyncrounous than others. Some more functional. Some more object oriented.
Your experience with the software you write in your day to day, is not the same stack and may not even be in the same universe as what others are attempting. Writing a React app is very different from writing a game in Unity, which is very different than writing a game engine in C++ which is very different that writing large scale services that talk to clusters of computers and hardware devices, which is different than writing some low level library in Rust or Haskell or doing data science in R or Octave.
Please fucking stop with this “one right way”, “you’re dodging it wrong” dogmatic bullshit. Your limited worldview does not apply to ALL of software development.
Use a fucking REPL if you want. Use a debugger if you want. Use logs and traces if you want.
Just try to write good software, try to understand your code, and ignore dogmatic and valiantly bad advice and opinions like the parent comment on Hacker News.
> This is perhaps one of the most harmful statements I’ve ever read on Hacker News.
And here I thought I was writing an innocuous comment on debugging code. Hyperbole much?
> There are a LOT of different ways to write software... Writing a React app is very different from writing a game in Unity, which is very different than writing a game engine in C++.
It's interesting that you brought up these 3 use cases. I started my career working for a shop that wrote a game engine in C++, then moved to Unity. I now write React apps for a living. Do I know you? :)
With the exception of a few corner cases, like portions of code that does rendering, and HFT software, where performance concerns trump everything else, what I have seen in that time is the following:
1) decomposing your code into smaller functions is objectively better than NOT doing so.
2) writing functions that don't mutate its local variables is objectively better than writing functions that DO.
By "objectively better", I mean that the code written conforming to the above have the following characteristics:
- more readable than code that does not.
- more testable than code that does not.
- more re-usable than code that does not.
It's on these two assumptions that I build my case for eschewing the debugger altogether in favour of the REPL. If you write code conforming to the above, you have absolutely no need for a debugger.
Having said that, it is possible that you need a debugger to wrap your mind around code bases that you inherit, which were written poorly (or equivalently, in a very "object oriented" fashion).
I don’t disagree with your principals of good code quality. I disagree that by following those principals you no longer need to use a debugger or to develop debugging skills. Your post is harmful because it suggest to junior developers that if they must use a debugger they must be developing their software in a poor fashion. This is not true. There are many good reasons to use a debugger, including to develop software or to explore a codebase and to debug your code.
> I disagree that by following those principals you no longer need to use a debugger or to develop debugging skills.
Using a debugger (as in, software where you step through code) and developing "debugging skills" are entirely different things. I was only commenting on the former.
> Your post is harmful because it suggest to junior developers that if they must use a debugger they must be developing their software in a poor fashion.
Would it be better if I said: "if you need a debugger, you have are either mucking around in poorly written code that you inherited or you're probably doing it wrong"?
> There are many good reasons to use a debugger, including to develop software or to explore a codebase and to debug your code.
I concede that exploration of a codebase is a valid use of a debugger.
Yes, this is why they are useful. Generally speaking once I've found where the error is, it's real simple to find the one or two variables in scope, look at their values, and figure out the bug.
I would agree that if you're using a debugger to verify over many iterations that something isn't going wacky with state, then yeah, that's not a great sign.
It is true that you can answer any question by changing the question.
It would be nice if everyone wrote code that minimized state, or if every problem could operate with minimal state. But not all code is or should be like that.
"to do away with assignments" is not my favorite way of phrasing what you mean either; I would prefer people assign as many new variables as they like, and just avoid mutating them (i.e. making them `const` and using immutable data structures if performance allows)
> I would prefer people assign as many new variables as they like, and just avoid mutating them
While I would agree that it is much more important to have functions NOT mutating their local variables, doing away with assignments is a straight-jacket which immediately forces you to decompose larger functions into smaller ones.
In fact, even in languages like Haskell where mutations of local state is entirely done away with, I see constructs such as `where` and `let` being abused to construct inordinately long functions.
Longer functions tend to accrue even more length over a code base's life and become harder and harder to test as they do.
Eventually function decomposition becomes a proxy for assignment anyway, especially in languages where assignment and mutations are necessary. Function decomposition is not necessary for all code and can actually be harmful to one's understanding if done improperly.
I generally follow the rule of threes ("if the code appears in three places the exact same way, extract it to a separate function") as this helps me balance readability of functions with size of functions.
I understand your advice applies very neatly to functional languages. I would love to live in a functional world. I don't. So that's why my advice is like that.
That might be true for library-type code, but anything operational is going to have environmental factors that can cause induced misbehavior rather than a pure "bug" from improper implementation. Good debugging tools help induce these things and monitor behavior. For example, I might have a timing problem that I suspect to be related to network, and I can simulate a lossy network.
I have found that code that needs a debugger is code that is written in an extremely stateful fashion.
The very reason why debuggers exist is to deal with code that has a dozen variables book-keeping complex state that you need to track closely to figure out how something works (or why something doesn't). Debuggers even have "watches" to track something closely so that it doesn't change underneath you, when you aren't looking.
To me this is a code smell. I would suggest that instead of using a debugger, we ought to be composing small functions together.
Small and pure functions only need an REPL to test.
Once you make sure the function is behaving right, you can almost just copy paste over the test cases you ran in your REPL into unit tests.
The simplest way to enforce this discipline is to do away with assignments, and instead constrain oneself to using expressions. This forces one to break code into smaller functions. Code written this way turns out to be much more readable, as well as reusable.
I suggest that we eschew the debugger in favour of the REPL.