#[derive(Debug)] by default

Having such a default impl will interact badly with lifetimes. Currently, you can write

impl Debug for MyType<'static> { ... }

This impl would be broken by the introduction of a blanket default impl, since it would require us to specialize on lifetimes.

1 Like

I disagree that having a lint solves this issue. The real problem occurs when working with generic code. For example:

fn do_something<T>(val: T) { ... }

When working on such a function, it would be nice to be able to print out the argument:

println!("Arg: {:?}", val): 

However, doing this requires adding a T: Debug bound. In order for this to compile, you now need to add a #[derive(Debug)] to every single type that is ever passed to this function. This may require several iterations, as a type may contain non-Debug types, and so on recursively.

If this function is called from another generic function, this can require propogating Debug bounds everywhere. If a non-Debug foreign type is passed to the function, then it's impossible to make this compile without further refactoring.

Note that the issue with generic code exists even if every single type in existence happens to implement Debug, since you would still need to propagate T: Debug everywhere. Therefore, no lint can ever solve it.

All of this is just to print some representation of a particular type.

I think some kind of auto-Debug would be extremely useful. However, I think it would almost certainly require some kind of additional compiler magic to work around the specialization issue described above, without breaking existing code.

7 Likes

Just in case people don't know, rustc -W help lists all of the warnings and their level. I personally run rustc -W help >> src/lib.rs every time I upgrade to a new version of the compiler and compare the new warnings with the old ones, just to see if I need to add/remove them.

I really hope rustc doesn't warn-by-default just because Debug isn't implemented for a type. This means that every little Rust code snippet will either be plastered with distracting #[allow(...)] or #[derive(...)] attributes or else you'll get useless and noisy error messages that are completely irrelevant to the little code snippet.

Auto-implementing Debug is fine IMO, as long as there's good dead-code elimination and function merging in the backend (and compile times don't take a hit... compiling Rust is already too slow).

4 Likes

Perhaps a reasonable middle ground would be a (Clippy?) lint that warns by default for exported types (i.e. visible to external crates) that do not implement Debug. This would help with the problem of downstream crates not being able to derive Debug on compound types, without adding any noise to to small test programs, binary crates, or internal types in libraries.

Edit: It turns out this is already how the built-in missing_debug_implementations lint works.

7 Likes

Yes, that's the entire point of parametric polymorphism. You are only allowed to use traits which are explicitly declared. This removes many bad surprises and makes the code much more easier to read, write, and maintain – in particular, one can be sure that once generic code type checks, it will compile and run correctly for all allowed instantiations. Let's not go back to C++ templates where this is not true, a major source of pain.

You should be adding those impls upfront. It's good practice anyway – after all, you might definitely not be the only one who will ever need to debug code using your types. Especially if those are public (but even if they are private, collaborators etc. may also need to interact with them).

And if there are non-Debug-able types, how do you think implicit derive should/could solve the problem? It pretty much can't. Unless the answer is "it should magically skip non-Debug fields/variants", which I hope it isn't, because that would entangle macro expansion with type checking badly.

Exactly. And that's a good thing, not a problem. A feature, not a bug. That is type checking in action.

Complaining about this is akin to complaining that you can't arbitrarily mix types around operators or that you can't put heterogeneous types into a Vec. It just doesn't make sense to allow this kind of behavior in a statically-typed language.

Please, let's just type those tiny #[derive] annotations. Make it part of your muscle memory, it's worth it.

It's not the only trait you'll want to derive anyway. I hope. Because there are many more which should be derived for interoperability, eg. Clone, Default, Ord, Hash, and so forth.

I don't think making these all magical(ly special) would be a good idea either – but why would they be different in this regard from Debug?

Furthermore, this is not the strongest argument by any means, but seeing which types implement which traits is very valuable. It saves you from having to constantly open the documentation of a type while reading the code. It's a nice little reminder about the capabilities of the type, it would be very annoying to give it up.

7 Likes

That's only true for traits without a blanket impl. For example, the following code compiles, despite the fact that I didn't write T: MyTrait anywhere:

trait MyTrait {
    type Foo;
}
impl<T> MyTrait for T {
    type Foo = u8;
}

fn bar<T>() {
    let a: <T as MyTrait>::Foo = 25;
}

To be clear, I am not advocating for post-monomorphization errors (a la C++ templates). My post is talking about making T: Debug hold for every T (via compiler magic).

The point of Debug is just to display some human readable representation of a type. It's not subject to Rust's normal stability guarantees - the output can change at any time, because the only thing it's useful for is being printed out to the screen. Something like MyStruct { field1: 25, field2: <missing_Debug> } is much more useful than seeing nothing at all.

Vec and operators have documented, well-defined, guaranteed-stable behavior. None of these conditions apply to Debug:

  • The trait description is just "Debug should format the output in a programmer-facing, debugging context."
  • The Debug impls for standard library types have changed before, which would be a breaking change in any other situation.
  • MaybeUninit's Debug impl just prints out the type name (since it can't know that the inner value is in a well-defined state). This reflects the fact that Debug is 'best-effort', and does not necessarily have any relationship to the structure of a type.

As I said earlier, having #[derive] (or impl Debug) for every single type in existence would have no effect on using Debug in generic functions.

It doesn't make sense to have impls of these traits for arbitrary types. There are many types which explicitly do not implement these traits, because it would be impossible or unsound to do so.

The 'best-effort' nature of Debug means that every type can have some Debug impl, even if it's not very useful (e.g. MaybeUninit). The reason why this is actually useful is that it enables printing something in generic functions, where the only alternative is to print nothing at all.

4 Likes

The idea would be to have some kind of 'compiler magic' which is able to provide Debug impls for types for which no user-written impl exists (whether impl Debug for T or #[derive(Debug)]). The #[derive(Debug)] expansion could continue to emit calls to Debug, which would then 'just work' for every type.

Getting this to work would be very tricky - simply using specialization won't work, due to the existence of impls like impl Debug for MyType<'static>. However, I think a solution would definitely be worth pursuing, as long as it didn't result in a significant increase in the complexity of the compiler.

2 Likes

I am uncertain if anything similar to Swift's implementation of debug printing is possible or acceptable in Rust, as it relies on global runtime metadata, which I believe Rust tries to avoid, but it is an explored point in the space of solutions to this problem.

It is described in moderate detail here, but it is based upon the function String.init<T>(describing: T).

This function looks for the global conformance records for the type's conformance to TextOutputStreamable, CustomStringConvertible and CustomDebugStringConvertible (respectively similar to Write, Display and Debug), in that order, and if it finds one it uses it and otherwise falls back to formatting the type metadata, which contains, among other things, the type's name and those of it's public stored properties.

Thus, in Swift, it is always possible to print something for a value.

Java's com.smth.else.Type@1234 is not bad either when you do not "specialize" toString()

3 Likes

It doesn't break here, it just "removes" the applicability of the impl ... default on non-'static monomorphisations of the struct: Playground

So it is not horribly bad (i.e., it can definitely be a starting point), but it does indeed show that such an impl in the current state of feature(specialization)] would not totally solve the issue, contrary to what I'd have expected, so thanks @Aaron1011 for bringing this up.

3 Likes

Yes, this is why the lint is not warn by default. I don't think we should change the default: warning on every private type in a crate not implementing Debug would be a very noisy experience for people while they are iterating.

1 Like

@withoutboats I think there's a strong case to be made for adding a default enabled warning for missing Debug impl on pub types, since users cannot add Debug to other library's types. So if a library doesn't impl Debug, that causes a lot of extra pain for users, and it's something which is easy to forget.

3 Likes

Could the paper cut most people are trying to solve be fixed by instead changing #[derive(Debug)] to be applied to types even if some of their fields don't implement Debug? Fields that don't implement Debug could be omitted from the output or use a default <no `Debug` impl> message. I think this could work with just a little bit of private specialization. It seems like this could solve most of the pain points here without rocking the boat too much.

2 Likes

This can't happen until specialization is stablized because macros have no access to type information.

That might work for solitary libraries on crates.io (putting aside the false positives in the cases where you don't actually want to provide a Debug impl for a type), but I think it would lead to bad UX in cases like rustc with many libraries that are easy to jump between and where marking one as pub so that it can be reached by some other library doesn't really mean much re. wanting to debug something.

3 Likes

@Centril In such cases it's not difficult to put a #![allow(missing_debug_for_pub_type)] in the lib.rs of the crate, just like with any of the already existing warn-by-default lints.

And it certainly wouldn't be the first time that Rust has put in lints to encourage library authors to follow best practices (there's plenty of those already), especially when doing so has a beneficial impact on downstream users.

If I have a struct, and one of the fields contains a library type which doesn't impl Debug, I now have to write a quite large and clunky custom Debug impl for my struct. The user experience is quite bad.

On the flip side, if the library author doesn't want a Debug impl (which would be quite rare, since it's good practice to impl Debug for pub types), they just use #[allow(missing_debug_for_pub_type)], which is very easy. So there's a clear convenience disparity here.

rustc lints does not follow Clippy's "liberal sprinkling of allows" policy. Instead, we have quite a low bar for false positives, and I think the rate of false positives is far too great here.

PS: Note that some of those already existing warn-by-default lints are ones that you should not allow (in fact, arguably you should not be able to allow them).

3 Likes

@Centril So how do you explain non-camel-case-types, non-shorthand-field-patterns, non-snake-case, non-upper-case-globals, unconditional-recursion, unused-parens, and while-true?

Some of those have high rates of false positives. Some of them are mere stylistic nits (like non-shorthand-field-patterns, unused-parens and while-true).

In practice I have encountered very few pub types that don't have Debug, precisely because it is very good practice to impl it, if for no other reason than for the sake of your users.

Thus, lacking Debug is almost always because the author forgot, and not an intentional choice. If it was intentional, well, that's what allow is for.

I don't see why Rust lints shouldn't support allow, or why Clippy lints would somehow be different. A lint is a lint regardless of what tool is doing the linting, the behavior is the same.

3 Likes

Most of these lints are quite old and somewhat grandfathered, though they have some value so we need not change anything there. Undconditional recursion is likely manifesting a bug. while_true is silly when loop { ... } can be used instead. I'm not sure that all of those would be lints today, but I've also not seen a lot of movement towards removing them.

I appreciate your experience, but I think it covers the case of a large project (e.g. an application) with many internal libraries, and long build-times, poorly. I also don't think omitting a Debug implementation is so bad, and in case a PR needs to be sent to fix it, it's a semver compatible change (or if not, then the impl probably shouldn't be provided).

As for why Clippy is different from rustc, the former is opt-in, whereas the latter isn't. That, to me, makes all the difference. A warn-by-default lint in rustc is a fairly firm push towards how code shouldn't be written.