Hacker News new | ask | show | jobs
by KeyboardFire 2712 days ago
It's funny that this is the first time I've seen a language explicitly condone "print debugging." It's one of those things that everyone says you're not supposed to do and then does anyway.

Does any other language have a similar feature?

11 comments

I think debuggers are not worth the cost for many kinds of debugging scenarios. They're great for stepping through projects you're not really familiar with or in situations where code seems to be in violation of baseline expectations, but fiddling around with breakpoints and watches and other UI particularities of the debugger carries more cognitive overhead than "print debugging". Additionally, I think the problem is better solved by using thoughtful and contextualized logging with appropriate severity levels. Couple this with a TDD approach to development and you'll end up in a workflow that is just faster than stepping through lines of code when you could have your assumptions verified through logs and test assertions.
Agreed! Personally I've found that I can find and fix problems much faster with a few print statements than with a debugger--debuggers can make it harder to trace through the full execution of a program.
> debuggers can make it harder to trace through the full execution of a program.

Except you often don't want a full execution, you often just want a partial execution where you suspect the problem arises. I can assure you that a good UI debugger is extremely helpful. Command line debuggers less so.

Yeah, I feel like people who don't like debuggers either don't use IDE's with amazing debuggers, or don't take the time to learn and understand how to use them. You can do powerful things with a debugger in seconds. I still use print statements under other environments. I think 'debug' logging is useful. I prefer logging over "print" any day of the week.
This is why unit tests are so helpful. You only debug the part of the code that's broken. It's a kind of a different way of thinking and people often write what I consider to be "bad" tests -- i.e. tests that don't actually exercise real portions of the code, but rather make tautological statements about interfaces. I spend a considerable amount of time designing my code so that various scenarios are easy to set up. If you find yourself reaching for a fake/mock because it is hard to set up a scenario with real objects, it's an indication of a problem.

This extra work pays off pretty quickly, though. When I have a bug, I find a selection of tests that works with that code, add a couple of printf-equivalents and then rerun the tests. Usually I can spot the error in a couple of minutes. Being an older programmer (I worked professionally for over 10 years before Beck published the first XP book), I'm very comfortable with using a debugger. However since I started doing TDD, I have never once used one. It's just a lot faster to printf debug.

The way I've explained it before is that it's like having an automated debugger. The tests are just code paths that you would dig into if you were debugging. The expectations in the tests are simply watch points. You run the code and look at the results, only you don't have to single step it -- it just runs in a couple of seconds and gives you the results.

You may think that the overhead of writing tests would be higher than the amount saved with debugging and if it were only debugging, I think that would be true. However, one of the things I've found over the years is that I'm actually dramatically faster writing code with tests compared to writing it without tests (keep in mind that I've got nearly 20 years of TDD experience -- yes... I started that early on). I'm pretty good at it.

The main advantage is that when you are writing code without tests, usually you sketch together a solution and then you run the app and see if it works. Sometimes it does pretty much what you want, but usually you discover some problems. You use a debugger, or you just modify the code and see what happens. Depending on the system, you often have to get out of the context of what you are doing, re-run the app, enter a whole bunch of information, etc, etc. It takes time. Debuggers that can update information on the fly are great time savers, but you still have to do a lot of contextual work.

It takes me some extra time to write tests, but running them is super quick (as long as you aren't writing bad tests). Usually I insist that I can run the relevant tests in less than 2 seconds. Ideally I like the entire suite to run in less than a minute, though convincing my peers to adhere to these numbers is often difficult. That 2 seconds is important, though. It's the amount of time it takes your brain to notice that something is taking a long time. If it's less than 2 seconds (and run whenever you save the file), usually you will barely notice it.

In that way, I've got better focus and can stay in the zone of the code, rather than repeatedly setting up my manual testing and looking at what it is doing. Overall, it's a pretty big productivity improvement for me. YMMV.

I really like the idea of tests as materialized debugging sessions.
Any multi-instance, multithread-capable language must have an appropriate debugger.

> [src/main.rs:4] x = 5

How to differentiate the value of 'x' for a given thread/instance/whatever? Don't add the info by hand to the debug message.

edit: typo

Using a debugger for testing multi-threaded code is particularly painful. Tests and logs are especially superior in this case because you can make complex assertions that capture emergent behavior of a multi-threaded application. Pausing threads to step through them can often make it harder to observe the behavior you might expect to see when multiple threads are working together in real time.
> I think debuggers are not worth the cost for many kinds of debugging scenarios.

That just means your debugger has a prohibitively high cost to use. If it takes more than 2 clicks to launch a full debugging session of your project, you need a new IDE.

The cost isn't in starting the debugger, it's getting into what you think is the right point in the execution of the program, and then stepping through one step at a time until you notice something is off.

With printf debugging, you can put print statements everywhere you think something might be wrong, run the program, and quickly scan through the log to see if anything doesn't match your expectations.

Most debuggers have conditional breakpoints and print-breakpoints, no need to step through line by line manually. If something strange happens just right-click and insert a print as the program is running.

With print-debugging if you find a bug you have to stop your app, insert print lines, recompile, redeploy, relaunch, click through your app to reach buggy location and then scan through log. This really feels like stone-age once you've ever used a IDE.

This "print debugging is bad" idea never made sense. Debugging is largely an effort to understand what's going on inside a program - having the program "talk back" to you via prints can be a great way to do this.

Sometimes, print debugging is the only practical way to fix a bug. For example, very rare bugs which can only be reproduced by running many instances of the code for a long time, or situations where attaching a debugger is not feasible (as in live services).

Then one uses trace points like IntelliTrace or DTrace.
Does this also work with 10 replicas of something in Alpine containers on clusters spread across different regions?
IntelliTrace works perfectly fine on Windows clusters and across Cloud deployments on Azure.

Never used Alpine containers, so no idea how it works with DTrace / SystemTap.

However a quick web search revealed the following right away, surely there are other results available.

"Systemtap for CoreOS Container Linux"

https://medium.com/makingtuenti/systemtap-for-coreos-contain...

"App Trace Roll: Users Guide" . For Red Hat Linux clusters

https://docs.huihoo.com/rocksclusters/app-trace/4.1/index.ht...

The person who proposed it is a (previous) Haskell developer.

The dbg! macro is definitely inspired by https://hackage.haskell.org/package/base-4.12.0.0/docs/Debug....

How so? In Rust, the macro is just a very thin wrapper around some output formatting boilerplate. In Haskell, it exists because you'd otherwise have to change the type of the function to add print-debug statements.
Because it can be used to wrap any expression.

I haven't seen a debug function that returns the value again anywhere else.

It was definitely in Arc first, and probably in other lisps before that:

https://github.com/arclanguage/anarki/blob/master/arc.arc#L1...

Edit: Yes, it's in Common Lisp too, so it goes back at least to the 1980s and probably further.

http://www.lispworks.com/documentation/HyperSpec/Body/f_wr_p...

The debug printing method p in Ruby have been returning it's argument for years now. Dbg seems possibly better for the usecase though as you get the location as well.
It also reminds me of Ruby's

  .tap { |x| puts x }
For those of you who don't know Ruby, its map/filter/reduce functions chain like this:

  values.map { |x| x + 2 }.select { |x| x > 3 }
So when you want to look at an intermediate result, there's the .tap() method that runs a lambda with that intermediate result, then passes it on to the next step in the chain.

  [0, 1, 2, 3].map { |x| x + 2 }.tap { |x| puts x }.select { |x| x > 3 }
This returns [4, 5] after printing [2, 3, 4, 5]. ("puts" is Ruby's println.)
Took me a while to find it, but Rust's iterators have an `.inspect` method that gives you a read-only reference, so println debugging works fine. For more advanced tap-like stuff, use the `tap` crate (which allows you to write `array.tap(|xs| xs.sort())` for example, even though `sort` mutates in place and doesn't return the array).
It's pretty much exactly like Ingy's XXX perl module from 2006: https://metacpan.org/pod/XXX

Prints a YAML dump of "my str", followed by file/line number information, then returns its argument so it can be embedded in expressions:

    $ perl -MXXX -E 'say uc(WWW("my str"))'
    --- my str
    ...
      at -e line 1
    MY STR
Data::Dump's "ddx" (1996) is also commonly used for print debugging, except it doesn't return the argument.
Almost. dbg! also shows the statement in addition to what it evaluates too. That is:

  dbg!(n * factorial(n - 1))
shows up as:

  [src/main.rs:5] n * factorial(n - 1) = 2
  [src/main.rs:5] n * factorial(n - 1) = 6
  [src/main.rs:5] n * factorial(n - 1) = 24
Although, IIRC there was some crazy dumper module by dconway (who else) that worked similar to this, but I don't recall if it returned the value.

...

So, I just looked it up, and I found it. Actually, Damien wrote two. One that he updated from 2014 to 2016, and one he started in 2017 and has maintained to the present. I have no idea the reason for that.

1: https://metacpan.org/pod/Data::Show

2: https://metacpan.org/pod/Data::Dx

Hehe nice ones, thanks for the links! :)

Yep leave it Damien. I remember he did something similar for a test module where it would show the expressions that were evaluated in the test failure output.

The `p` function in ruby is designed for quick pretty printing. `console.` in the browser are only* useful for debugging. Go has a `println` keyword so that you don't need to import the `fmt` package, and has a `%+v` format helper to get a pretty printed debugging friendly representation of an object.
As a new go programmer you just saved me a bunch of time, I didn’t know about either of those.
Elixir has a very similar function, IO.inspect(..), which prints a debug version of its argument and then returns it, very similar to dbg! here. Coupled with Elixir's pipeline operator `|>` which passes the output into the first argument of the next function, it's pretty handy: foo() |> IO.inspect |> bar()
Print debugging is only necessary when support for trace points in debuggers is lacking for the respective language.

With good support, a couple of trace points, even on a live running instance is all that is needed, without any extra recompiles.

This is just not true. For example, some bugs are so rare that you can't hope to reproduce them by just looking at "a live running instance". In this case, print debugging can be the only practical way to go.
Which is exactly what trace points are for.

More devs should learn about JTags, IntelliTrace, DTrace, ....

No need for manually writing printf-debugging and recompiling all the time, when the debugger can do that for you.

Do people really not want people to print debug? The professors of all my CS classes so far (from data structures to assembly) explicitly tell students to print debug.
When you're learning it's fine.

I would hope at some point you would get to the point where you move past this, once you get to Enterprise scale print debugging stuff would be slow and laborious.

Elm has a Debug.log function which takes a string and any value, prints "String: value" in the console, and then returns the value.
Bash has a -x mode that does this.
Python/Ruby have long had a tradition of printf + unit test debugging. I assume Python has a debugger now but I honestly don't know anyone who uses it.
I use it all the time. If my program crashes, I swap `python foo.py` with `ipython --pdb -- foo.py` and it will drop me into an ipython session at the exception so that I can inspect variables and the backtrace.

Also, if I want to pause at a point and step from there, just drop `import ipdb; ipdb.set_trace()` at the line I want to set a breakpoint.

I use it all the time as well and know many people that do. It was no longer than yesterday that I discovered it worked in a jupyter notebook ! So I assume enough people use it that it was integrated
I was using Python debuggers in 2002, courtesy of ActiveState.
print is a considered a big antipattern in python since it's equally easy to use the built in logging.debug. With pycharm launching the debugger is as easy as right-click a file and press debug, i don't know anyone who doesn't use it.