Requirements for deriving Debug on types containing associated types

In this example there's an enum I'm trying to derive Debug on, copied here for convenience:

trait SomeTrait {
    type ASSOCIATED;
}

struct HasAssociated;
#[derive(Debug)]
struct IsAssociated;

impl SomeTrait for HasAssociated {
    type ASSOCIATED = IsAssociated;
}

#[derive(Debug)]
enum SomeError<T: SomeTrait> {
    Variant(T::ASSOCIATED)
}

fn main() {
    let err: Result<(), SomeError<HasAssociated>> = Err(SomeError::Variant(IsAssociated));
    err.unwrap();
}

The enum's generic parameter does not actually appear in any variant of the enum, instead it's required to implement a trait with an associated type which will be contained in one of the enum's variant.

To derive(Debug) rustc currently requires that both the enum's generic parameter and the associated type implement Debug, despite the generic parameter never actually appearing in the enum.
The same applies for structs using the associated type in a field.

Is this intended behavior? I don't think a type parameter that doesn't actually appear in the enum/struct should be required to implement debug.

What happens when you manually implement Debug for SomeError?

If that works then it's a limitation with the derive macro. That would make sense because it's very much nontrivial to do any sort of type analysis in proc-macros, which leads to all kinds of shortcuts being taken to both keep things workable for the user and maintainable for the maintainer(s).

I tried using cargo-expand and it seems that the derive macro is adding the requirement for Debug on T:

#[automatically_derived]
impl<T: ::core::fmt::Debug + SomeTrait> ::core::fmt::Debug for SomeError<T>
where
    T::ASSOCIATED: ::core::fmt::Debug,
{
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        match self {
            SomeError::Variant(__self_0) => {
                ::core::fmt::Formatter::debug_tuple_field1_finish(
                    f,
                    "Variant",
                    &__self_0,
                )
            }
        }
    }
}

Removing the requirement makes the program compile (using nightly rust for the fmt helpers)

One quick fix is to use (a cleaned-up version of) the expansion, minus the T: Debug trait bound, rather than the derive.

When applied to a generic type, all derive macros currently work by naively adding a T: Trait bound to the derived impl for every type parameter T of the type, regardless of whether it’s actually needed. This is a "works 99% of the time" solution – in cases it doesn’t work you can sinply impl the trait(s) manually. However, it wouldn’t be that difficult to change the macros such that they’d work in cases like this as well; rather than expanding

#[derive(Eq)]
struct Foo<T, U> {
    a: A,
    b: B,
}

(for some arbitrary types A, B) as

impl<T: Eq, U: Eq> Eq for Foo<T, U> {}

the macro would output

impl<T, U> Eq for Foo<T, U>
where 
    A: Eq, B: Eq
{}

I don't know that it's simple, but this idea is known as perfect derive. It would be a breaking change for the existing macros to become perfect derive macros (in addition to having a higher semver hazard, as discussed in the blog post).

1 Like

That seems to imply that a crate could take this on successfully, assuming perfect derive is actually computable.

That last part isn't clear to me because, how can a macro figure out on its own whether the bound is necessary or not?

An alternative could be to do what e.g. the serde derive macros do, and add support for macro parameters so that users can specify what it is they want the macro to do.

I have the opposite impression -- since a new trait solver is required, and you need the ability to query existing implementations and the like. I.e. you need language support. I'm pretty sure I've seen another perfect derive post in the context of a-mir-formality, with more details, but I couldn't find it offhand.

I think both have merits. This explicit approach gives you more control (and so is less of a semver hazard). I don't think it's a huge lack in the context of std derive though, since those traits are much easier to write by hand than the serde ones IMO.[1] But it'd still be a nice improvement; I'm pretty happy being able to derive(Default) on enums now.


  1. However maybe that just speaks to my (lack of) experience. ↩︎

They cost less effort than especially serde::Deserialize, as that is the trait that can get really involved to write by hand¹. I've had to do it a number of times, and it's never fun. The inverse, serde::Serialize is about on par with std::fmt::Debug I'd say.

Yeah being able to derive a trait impl is always nicer than having to implement a trait manually, both because it's generally shorter and because it's essentially declarative rather than imperative programming.

¹ To date serde::Deserialize is the only trait where in the process of writing an impl for it I write another trait impl inside of it. That other trait impl is the Visitor to traverse the serialized data. The other part of the complexity comes from the fact that it can borrow the serialized data.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.