Private derive(Default) ergonomics [PDDE], vs blanket impls, vs other alternatives

So like, it'd be nice if either of these were a thing:

PDDE

PDDE makes it so the behaviour of #[derive(Default)] changes for private structs. instead of adding bounds on type parameters, it would add bounds on field types. This is leaky, but because private structs aren't public, it only leaks into private API details... which are private anyway.

Blanket impls for local traits

TL;DR:

trait Foo {
}
impl<T: Foo> Default for T {
  fn default() -> T {
    panic!("Foo should be uninhabited.")
  }
}

Either of these would allow derive(Default) to work for us: the trait we're dealing with is supposed to be uninhabited anyway, as it doesn't take self anywhere. and everywhere it's relevant, we have an Option or some other thing that impls Default in the way.

Ppl don't seem to want either of them tho. PDDEs get called "too surprising" (when making something pub or private the bounds change - altho honestly we'd say refactoring can break code and you're supposed to fix said breaks when refactoring) or "too leaky" (because it leaks the field types by defining bounds based on them). and blanket impls for local traits are "too restrictive" for downstream crates (which... well, yeah, but that's intended).

Thoughts?

This sounds like as if PPDE would be an abbreviation of something and like there were previous discussions on these topics before (“Ppl don't seem to want either of them tho.”)

Edit: Oh, it’s in the title! “ Private derive(Default) ergonomics”

Would you mind resolving the acronym and also, in case you have them at hand, add links to previous discussions?


Also, I personally like the idea of allowing blanket impls for local traits.

For the derive macros, I think it would be nice if there was a way to speficy the bounds manually. E.g.

#[derive(Default)]
#[default_where()] // ensures that `T: Default` isn’t required
struct S1<T> {
    field: Option<T>,
}

#[derive(Default)]
#[default_where(<T as ToOwned>::Owned: Default)]
struct S2<'a, T: ?Sized>
where
    T: ToOwned,
{
    field: Cow<'a, T>,
}

then, specifying no where clause would be the same as the current behavior, T: Default for every type parameter T. In either case, the actual Default implementation has a combined where clause consisting of the struct’s own where clause and bounds, and additionally the explicit or implicit additional constraints.

Maybe this can also lead to a way to make PPDE less confusing, i.e. make them explicit, something like

#[default_where(_)]

or

#[default_where(...)]

or

#[default_where(inferred)]

that only works for non-pub types.

All of this could be tested in an external crate with a new, alternative, Default derive macro.

A need for derive ergnonomics like that or a way to explicitly give the where-clause also applies to pretty much every other trait in the standard library that can be inferred, right?

1 Like

Isn't this a breaking change, though? (I do realize it's only about private types, but private code can be broken too.)

Indeed! But it's a particular pain point with Default (and sometimes Clone - when using Rc, for example) because generally things like PartialEq, etc aren't gonna be implemented by ignoring generics.

We can't come up with a case where having more Default impls would cause code to stop working. That isn't to say they don't exist, however.

You'd have to:

  1. Be using #[derive(Default)].
  2. On a private struct.
  3. With type arguments.
  4. And somehow use it in a way that breaks if the struct happens to become Default.

It should be only adding more implementations of Default, right? In light of the fact that we don’t have specialization yet, this should not really be noticable by code that compiles today. (Unless I’m missing something.)

2 Likes

For what it's worth, twaking derive(Default) is a local change whereas tweaking the type system is... well, that's tweaking the type system. It's a lot more complicated.