Pre-RFC: MaybeDebug<T> type which always `impl Debug`

Please let me know what you're thinking about it. Also, should I keep the empty sections on real RFC?

Summary

Adds struct MaybeDebug<T>(&T);. which implements Debug trait regardless whether the T implements it.

Motivation

Debug trait is used for the programmer-facing, debugging context formatting. Most public types implements this trait to enable casual debugging. But since it's a regular trait not something built into the language, you need to explicitly adds trait bound to use it in the generic context. This can be painful with deeply nested generic code, and sometimes it affects the public API of the crate which is not needed for the functionality.

Since it's testing/casual debugging, we usually know from the outside that the actual type passed into have the Debug impl. What does it prints if it's not is not much important as debug logging doesn't affect the actual functionality. For casual debugging in such context we need some way to print its Debug format if it has one, print some dummy text otherwise.

Guide-level explanation

Adds struct MaybeDebug<'a, T>(pub &'a T); type into the core::fmt module. This type implements Debug trait regardless whether the T does it. Its implementation transparently forwards to the T's implementation if it exist. If not, It prints the core::any::type_name::<T>() instead to point you what to fix if you want more information.

debug!("what happens here: {:?}", MaybeDebug(&client.conn));

Reference-level explanation

Adds struct MaybeDebug<'a, T>(pub &'a T); type into the core::fmt module. It has one base implementation of the trait Debug, and a specialized implementation of the same trait for the T: Debug case. The base implementation SHOULD provide enough information to casually identify what the concrete type T is. The formatted output of the specialized implementation SHOULD be identical with the output from the T's Debug implementation. In both cases, the exact output format is not considered stable.

Drawbacks

It adds up additional API surface to the stdlib.

Rationale and alternatives

It can be implemented as a 3rd party crate. But since it uses the specialization feature, users with the stable compiler can't use it. And it doesn't seems like the specialization feature can be stabilized soon.

The field T can be stored by-value. This would simplify the type signature and would be more idiomatic if T: Copy. Non-Copy types can still be used as references due to the Debug implementation of the &T though it affect the output of the type_name::<T>().

Instead of the wrapper type, we can add another extension trait with method returning Option<&dyn Debug> with specialized wildcard implementation. This allows more fine grained control.

Prior art

Unresolved questions

Future possibilities

2 Likes

AFAIK you should either try to fill the sections or point out there is none. Well at least for prior art and unresolved questions. I'm 100% certain there is unresolved questions (see e.g. my things pointed out below), and I wouldn't be surprised about noteworthy prior art either.

These sentences are hard to understand.


Some prior context that I'm aware of:

asks for dbg! macro equivalent without Debug bound, with my answer

providing example code including a struct DebugWrapper<'a, T: ?Sized>(&'a T).

Quoted in a later question

(which also links another thread with a similar question)

where I point out soundness issues with the DebugWrapper of my previous solution

These soundness issues are a huge unresolved question!

They mirror the inherent soundness issues of the specialization feature itself; you just cannot specialize on T: Debug soundly at the moment. I'm not positive that these issues can be resolved significantly earlier than when the soundness issues of the specialization feature in general will be resolved. The wrapper you propose in this Pre-RFC can not be implemented using the safe(r) min_specialization feature alone, as far as I'm aware.

The only sound thing (that I know of) that you can do right now would be something like struct MaybeDebug<'a, T: 'static>(pub &'a T);, but that's of severely limited use.

6 Likes

I wonder if a hack akin to StructuralPartialEq could be featured here, to circumvent the soundness issue:

  1. provide an unnameable (for third-party code; e.g., private-ish and/or unstable) and/or unsafe marker trait:

    #[marker_trait]
    pub unsafe trait SpecializableDebug : Debug {}
    
  2. Which would be auto-implemented by using #[derive(Debug)]: the autogenerated impl would replace the Debug bounds with SpecializableDebug bounds.

    • It would be able to loosen the bound to just Debug for the type parameters carrying an explicit 'static bound (thus supporting fields with manual impls of Debug provided they were guaranteed never to be instanced with only-lifetime-distinguishable types).

Then a sound MaybeDebug wrapper could be featured by the standard library, but one which would fall back to not calling the Debug impl the moment an unsound specialization pattern were to be remotely possible.

  • As an example, with it, @steffahn's MaybeDebugStatic pattern could be featured:

    #[repr(transparent)
    #[derive(Debug, RefCast)]
    struct Static<T : 'static>(T);
    
    /* // The `#[derive(Debug)]` would generate:
    impl<T : 'static + Debug> {Specializable,}Debug
        for Static<T>
    { … }
    */
    
    type MaybeDebugStatic<'lt, T> = MaybeDebug<'lt, Static<T>>;
    
1 Like

Other prior art, @dtolnay recently used the autoref specialization trick to detect Debug impls in anyhow::Ensure Perhaps the same trick could help here?

I don't think there is any reason to use &T instead of T since impl Debug for &T where T: Debug

you can always do:

let a = "foo".to_string();

let b = MaybeDebug(&a);

I know that hacks like this allow you to mimic specialization:

macro_rules! debug {
    ($object:expr) => {
        {
            struct __Helper<T>(T);
            impl<T: std::fmt::Debug> __Helper<T> {
                fn __debug(self) -> T{
                    dbg!(self.0)
                }
            }
            trait Default<T> {
                fn __debug(self) -> T;
            }
            impl<T> Default<T> for __Helper<T> {
                fn __debug(self) -> T {
                    dbg!("default message.");
                    self.0
                }
            }
            __Helper($object).__debug()
        }
    };
}
struct NotDebug; 
debug!(10); 
debug!(NotDebug); 

Unfortunatly, when used on a generic context, it will always go with the default value. I think that specialization should be completely solved, and then this functionality could be retrofitted to dbg! without having to introduce a new struct.

Specialization can only be solved when implementations are guaranteed to apply to all lifetimes if they apply to some lifetimes as typechecking happens before erasing lifetimes (and thus may see that a specialization doesn't apply due to a lifetime bound), but codegen happens after erasing lifetimes (and thus doesn't know that a lifetime bound isn't satisfied). To make MaybeDebug work with specialization, you would either have to restrict all implementations of Debug to satisfy this rule (breaking change) or you have to somehow make MaybeDebug not implement Debug when there is a potential specialization for which the lifetime bounds are not guaranteed to hold.

1 Like

Yeah, personally, I think there is no solution to the unsoundness other than falling back to the less specialized implementation and emitting some sort of lint. If thats not an acceptable solution, then I doubt it will ever hit stable.

Another one for prior art is the crate debugit -- I've used it, but quite sparingly. Also didn't see a path to min_stabilization compatibility, so outlook unknown.

I think the RFC is a good idea - and it should be integrated in to dbg! IMO (But tactical note: this might not be a good thing to put in the RFC) (It was mentioned in the dbg rfc discussion). But we can only do this if there is a viable way to use stabilization.

In RFC: Quick dbg!(expr) macro by Centril · Pull Request #2173 · rust-lang/rfcs · GitHub (not the accepted one), this was called WrapDebug, though the latest draft of that RFC didn't propose exporting it: https://github.com/Centril/rfcs/blob/rfc/quick-debug-macro/text/0000-quick-debug-macro.md#specialization-and-non-debug-types

The accepted dbg! RFC conversation touched on it as well (https://github.com/rust-lang/rfcs/pull/2361#issuecomment-373636118), but ended up leaving it as something to consider in the future.

1 Like

Thanks for the feedbacks and references!

Sorry for the bad eng :slight_smile: I'll fix it later.

I wasn't aware of the soundness issue. Yes, it doesn't seems worth to implement it within the stdlib when the specialization feature is expected to be stabilized soon. For the T: 'static bound I believe it covers lots of use cases enough to justify its questionable API. But it should be included in the "Unresolved questions" chapter.

I personally think it's too much for this feature, interesting approach though.

For tricks using macros and method resolution order, I tested but it doesn't work on generic context.