I have seen devs scoff at the thought of print debugging, but I recall that in systems programming there are many times you can’t use a debugger or need to rely on other tool.
I’d rather schools teach the concept of step debugging vs runtime debugging. Teach students to try to understand the code, make hypotheses, and verify them.
I have seen some people use a debugger solely because they only know how to step debug. Meaning they start from main or another entry point and step through every line of code.
My point being that, you can’t judge a developer by if they use print statements or a debugger. Judge them by the methodology of how they debug.
Next time they do, ask them to recommend a better method of debugging that works across generally all languages, compilers, IDEs, and platforms with next to zero configuration.
Taking a program trace from the print debug statements, grepping through it repeatedly to filter down to certain events of interest, looking at the interleaving of those events, and figuring out the order that things happened in to cause it to go off the rails. That sort of thing. (To be fair, time-travel debuggers can start to get at this, but those are pretty uncommon. Traces, as you say, work almost everywhere.)
Or better yet, compare the traces between working and non-working runs to see how they differ. I've looked at diffs of traces this way before. (Sometimes I'll first use a small script to renumber pointers in traces by order of appearance.)
I've also used this sort of strategy before for debugging rare threading or other non-deterministic issues. Have the shell run the program in a loop, saving each run's trace and results to a different file, go off and get lunch, come back and see if anything failed. Then look to see if any of the runs failed and look for the structural differences between the working and non-working traces.
I can't imagine sitting and stepping through in a debugger 100+ times in the hopes that maybe this time, it will be the run that's just different enough to trigger the bug and that the debugger itself won't prevent the issue from manifesting. Not to mention, trying to remember the steps from all the good runs and spotting where the bad run goes bad before you've stepped to far. No thank you.
I think people really underestimate print debugging. Debuggers are fast and easy for simple bugs, sure, but there's powerful stuff that you can only really do with printed traces.
Isn’t that where things like breakpoint conditions, data breakpoints (“break whenever X value/field changes”), and dependent breakpoints, work pretty dang well? You just set up the appropriate situation and let it run until it breaks
But one thing that I have found helpful in the past has been aggregating the information from the traces across runs. Comparing good with good and bad with bad to classify the commonalities, and then compare good with bad to see how they differ.
The debuggers in most popular IDEs (IntelliJ IDEA/any JetBrains IDE, Visual Studio, VSCode, Eclipse) work the same way. You set breakpoints (typically by clicking somewhere around the line number), step into and out of functions, and look at the memory state. If you learned how debugging works in IDE X, you can easily switch to IDE Y without having to learn much.
Not just systems programming, but fixing issues in scaled production systems as well. Have fun attaching a debugger to the process that got killed twenty minutes ago when the spot instance it was running on got reclaimed. If you don't collect telemetry, you're blind. Debuggers are a luxury that you get to enjoy for issues you have before you ship.
Unless you have a setup where you can easily run one system with a debugger attached while connecting it to everything else, you’re basically restricted to running a debugger for bugs that can be reproduced locally.
Now maybe if I was better that wouldn't be the case, but even the cleanest "wish i thought of that" functional code i've seen still looks like it'd be easier to fail fast using a debugger with.
In fairness though, I will admit I use the debugger a lot less when i'm not screwing with reflection on generic types or whatever because runtime errors just happen a lot less in functional styles. Usually if it compiles, it runs, because the compiler can sanity check the code better than you can.
> [uses F#]
That has to be humblebragging. The average .net developer is terrified of or doesn't even know about F#.
F# isn't hard, it's just different. Hell in many ways i'd argue it's much much easier once you get used to it. It doesn't have the support C# does so often you're stuck with a library that WILL work but doesn't have documentation for doing it in F#, and that can lead to struggles, but that's not really a sign of being a good coder.
Most dotnet developers could probably code circles around me in F# if they knew it existed/gave it a chance.
Personally I stuck with it because it had the low code look of python with strong typing. It took a bit to wrap my head around some functional stuff (basically map/iter = foreach and if you want to update something on each loop you probably want a fold, or more likely a built in function), but once I got over that hurdle it was pretty smooth sailing.
The irony is that by far the hardest part is the library thing, which your average dotnet dev would handle WAAAAY better than me.
Oh? Did you win some award? I'm curious as to how you're determining this?
It's easy for people with functional programming knowledge, or who had OCaml as their first programming experience like I did at university, but for people without those exposures I can understand the difficulty.
The only reason F# winds up feeling harder isn't so much how F# works, but simply because the entire dotnet environment was built for C# first, so you do need to know how to handle the C# style. I do think that if you could just pick one and then suddenly have every library support it's styles, F# would probably be easier overall because it's just got a lot of nice features built in that make updating your code so much easier.
C# is not a simple language, but F# has basically all the complexity of C# with OCaml on top.
Either C# or OCaml would be simpler to learn, although the combination is powerful.
`f(a, b)` looks like a call to a function that takes a single tuple as its argument. So I would expect `f` to have the type signature `A * B -> C` instead of `A -> B -> C`. Is my intuition wrong? If it is, then what does F# use the parenthetical syntax for?
[1] https://learn.microsoft.com/en-us/dotnet/fsharp/language-ref...
F# enforcing a lot of good practice code style stuff (order matters for example pisses off long time devs who already have styles, but prevents SOOOO much stupid bs from beginners) and basically eliminates runtime/chasing variable state errors completely so long as you can stay within style. Yes it'd be nicer if there was only one way to invoke functions but if i had to take a tradeoff I think it's a pretty easy one.
It is an issue that yes, like your example, you're often stuck ALSO learning OO because "oh you want to use X library, well that's OO so...", and even then you can isolate your mutable/OO areas really well, but this is more of an issue with it being an second fiddle language. If F# got F# specific libraries for all the C# stuff out there tomorrow I think it'd take off and most people would never look back.
If we're talking basic business logic/beginner programmer stuff, yeah I think F# offers a lot of stuff that makes it flat out easier to use. And if you want to point out complex issues, I feel the biggest one is that something that's intuitively much easier to understand in OO (create a variable/object, populate it on each iteration of a loop depending on logic), can feel daunting as hell in F# (fold).
You're noticing a very real relationship.
One major issue I do see coming from the C# side is "well how do I do this then?!", which often the answer is "you don't, because you don't need to" or "well what if it's more performant to do it mutably!" well then thankfully F# can absolutely do that.
If you keep an open mind it's really a very clean and simple language, but in an age where half of development is importing 8 well known libraries, not being the main supported language is a major weakness.
However, I wrote my first thing a port of a small C# tool of a couple hundred lines, to F# in about an hour. It's been about a week now, and things are considerably smoothing out.
To be considered niche, the F# tooling has been great. Also, having the .NET libraries available adds a lot of built-in capability.
What I find is that if my code isn't working, I stop what I'm doing. I look at it. I think really hard, I add some print statements and asserts to verify some assumptions, and I iterate a small handful of times to find my faulty assumption and fix it. Many, many times during the 'think hard' and look at the code part, I can fix the bug without any iterations.
This almost always works if I really understand what I'm doing and I'm being thoughtful.
Sometimes though, I don't know what the hell is going on and I'm in deep waters. In those cases I might use a debugger, but I often feel like I've failed. I almost never use them. When I helped undergrads with debuggers it often felt like their time would be more productively spent reasoning about their code instead of watching it.
Are you aware of the amazing Java debugger feature of "Drop to Frame"? Combined with "hot-injection" (compile new code, then inject into current debug'd JVM), it is crazy and amazing. (I love C#, but the hot-injection feature is much worse than Java -- more than 50% of the time, C# compiler rejects my hot-injection, but about 80% of the time, JVM accepts my hot-injection.) When working on source code where it is very difficult to acquire data for the algorithm, having the ability to inspect in a debugger, make minor changes the the algorithm, then re-compile, inject new class/method defs, the drop to frame, the re-exec the same code in the same debug session is incredibly powerful.
The plus side of less capable tooling is it tends to limit how complex software can be--the pain is just too noticeable. I haven't liked java in the past because it seems very difficult without the tooling and I never had to do enough java to learn that stuff. Java's tooling does seem quite excellent once it is mastered.
If you are a low level programmer, I understand your sentiment. A piece of advice, when you need to use Java (or other JVM languages), just submit to all the bloat -- use an IDE, like IntelliJ, that needs 4GB+ of RAM. The increase in programmer productivity is a wild ride coming from embedded and kernel programming. (The same can be said for C#.)
When you're extremely "fluent" in programming code and good at mentally modelling code state, understanding exactly what the code does by looking at it, stepping through it doesn't typically add all that much.
While I do use a debugger sometimes, I'll more often form a hypothesis by just looking at the code, and test it with a print statement. Using a debugger is much too slow.
This varies, but in a lot of environments, using a debugger is much _faster_ than adding a print statement and recompiling (then removing the print statement). Especially when you're trying to look at a complex object/structure/etc where you can't easily print everything.
Don't remove the print statement. Leave it in, printing conditionally on the log level you set.
Early on it was a godsend. Start program, hit breakpoint, look at values, step a few lines, see values, make conclusions.
Now I rely on print statements. Most of all though, I just don't write code that requires stepping. If it panics it tells me where and looking at it will remind me I forgot some obvious thing. If it gives the wrong answer I place some print statements or asserts to verify assumptions.
Over time I've also created less and less state in my programs. I don't have a zillion variables anymore, intricately dependent on each other. Less spaghetti, more just a bunch of straight tubes or an assembly line.
I think it's possible that over the years I hit problems that couldn't easily be stepped. They got so complicated that even stepping the code didn't help much, it would take ages to really understand. So later programs got simpler, somehow.
This could be causal.
Besides, every IDE has a different way to debug, so they might just not be familiar with the interface. I can’t tell you exactly how to debug in VSCode even though I’ve used it the most. I’ve had to run a debugger only a handful of times in the past couple of years and it’s always for codebases that are more tangled (e.g. .NET where there’s interfaces everywhere).
Neither situation was at all related to my talent (or lack thereof).
I’ve been programming in C for nearly 20 years and primarily used printf for debugging for the first 12-15 years, and have used debuggers more and more. I use Emacs, and its gud mode is so nicely integrated into everything that using gdb is truly much, much faster than the alternative. I don’t use print debugging at all anymore.
It takes less time and cognitive overhead to just stop the program on the same line you would have inserted your printf, but you can now inspect the entire program state.
Obviously I’m not saying anything revelatory if you use an IDE on a regular basis… I guess this comment is for the folks who eschew IDEs.
Logging alone is fine, but it can often be difficult not to drown in information. I do embedded programming in C mostly.
I work on compilers and debuggers are more of an hindrance than help when trying to fix a compiler bug.
I believe there is a similar situation for distributed systems.
I used to read every help file on windows / visual c++, the FreeBSD manual, plowed through the file system and tested this it. Just because I was curious