Making build-in traits less magical

The behaviour of things like Copy/Send/Sync always seem magical because of the rules that makes the compiler automatically implement them or prevent to implement them for new types.

Rfc 1935, while concerned with a different goal made me wonder if it would not be preferable to have rather some automatical “magical” traits for classes of types (tuples, enums, structs, arrays, functions, closures, refs) that would then be called Tuple, Enum, Struct and so on… Those trait could then contain as associated types the list of their inner details and then allow to have standard blanket implementation to express ideas like "type X must be Send if the types of all its internal details are Send" (Of course for Copy that would require negative bounds and it’s unclear to me if this is a dead idea now). Has any similar suggestion already been proposed ?

Those trait could then contain as associated types the list of their inner details and then allow to have standard blanket implementation to express ideas like "type X must be Send if the types of all its internal details are Send"

Could you elaborate on this part? I can't really tell what you're suggesting or how it would improve anything. Maybe some strawman pseudocode?

Copy isn’t automatically implemented.

Using notation (H, ...T) from rfc 1935 (Tuple-Based Variadic Generics) to mean some non empty tuple with a first element of type H (head) and a rest equivalent to the tuple of type T (tail):

impl Send for () {}
impl<T, H> Send for (H, ...T) where T : Tuple + Send, H: Send {}

Similarily for structs if we imagine another automatic trait:

trait Struct {
    type Details: Tuple;
}
impl<T> Send for T where T : Struct, T::Details : Send {}

where for example if we have struct Point { x: f64, y: f64}, we would have Point::Details == (f64, f64). Same kind of logic for enums, closures, etc

Correct but the implementation is automatically prevented by the compiler if your type contains at least one non-copiable component. To this respect this is as magical as Send or Sync.

Now suppose we have negative bounds we could encode this fact as:

impl Copy for () {}
impl<T, H> !Copy for (H, ...T) where H: !Copy, T : Copy {}
impl<T, H> !Copy for (H, ...T) where H: Copy, T : !Copy {}
impl<T, H> !Copy for (H, ...T) where H: !Copy, T : !Copy {}

impl<T> !Copy for T where T : Struct, T::Details : !Copy {}

Of course as such I'm replacing previous magical traits (Send, Copy, etc) by new ones (Tuple, Struct, etc) but I have the feeling this would be more natural and far more extensive (allowing users to define their own automatically implemented "structural" traits)

You can already define your own structural traits with an implementation for ..: https://doc.rust-lang.org/src/core/marker.rs.html#49. The only difference here is that you’d have separate blanket implementations for structs, tuples, and enums.

I didn’t know syntax … and it’s rather unclear to me what it actually mean. Any documentation about this ?

This is indeed similar to what I’m proposing. But to be honest I find syntax “…” very ugly. This feature also feels very ad hoc for Sync and Send and not as expressive as if you could analyse the content of your types.

Could you give an example of an auto trait that would need to perform more advanced analysis?

Imagine for example that you would like to have a trait that represents the potential use of NaN:

impl ContainsNAN for f32 {}
impl ContainsNAN for f64 {}
impl<H,T> ContainsNAN for (H, ...T) where H: ContainsNAN , T: Tuple + !ContainsNAN {}
impl<H,T> ContainsNAN for (H, ...T) where H: !ContainsNAN , T: Tuple + ContainsNAN {}
impl<H,T> ContainsNAN for (H, ...T) where H: ContainsNAN, T: Tuple + ContainsNAN {}
impl<T> ContainsNAN for T where T : Struct, T::Details : ContainsNAN {}

More generally if we add some (unsafe I guess) methods to inspect the content:

trait Struct {
    type Details: Tuple;
    unsafe fn details_as_refs(&'a self) -> Self::Details::AsRefs<'a>;
    unsafe fn details_as_muts(&'a self) -> Self::Details::AsMuts<'a>;
}

where AsRefs<'a> and AsMuts<'a> are defined as in rfc 1935, we would get the capacity to naturally generate implementations for PartialEq for example rather than having a special macro to make the compiler generate it. This would provide a more homogeneous type system.

I think this kind of system may really expand expressivity. Of course the downside would certainly be that it would also allow some new forms of abuse. But the current rule for .. with opt out implementations feels some much of a hack to me (the rule that opts out your type if one piece of inner data is already opted out is completely implicit for example) that I’m really convinced we can do far better.

When would you use ContainsNAN in a bound? It seems strange to want a function that wants types that have a NAN somewhere inside of them, rather than types which don’t have a NAN anywhere inside of them. The inverted variant is trivially implementable with the existing infrastructure:

trait NoNANs {}
impl NoNANs for .. {}
impl !NoNANs for f32 {}
impl !NoNANs for f64 {}
1 Like

Because for example you would like to have special treatment when the value NaN is actually encountered:

trait ContainsNAN {
  fn contains_nan(&self) -> bool;
}

Of course for actual implementations you would need the capacity to process variadic tuple.

I mean, you could do that via a separate trait with specialized implementations for your auto-trait.

The ContainsNAN trait still seems like a really contrived example, though. What’s an actual thing that you can to do but can’t right now with auto traits?

There is also quite a lot of magic involved in the proposed Tuple, Struct, Enum, etc traits. They would need to not be implementable manually like any other trait, and known to the compiler to never overlap on a single type which is not the case for any other set of traits I’m aware of.

There is also quite a lot of magic involved in the proposed Tuple, Struct, Enum, etc traits. They would need to not be implementable manually like any other trait, and known to the compiler to never overlap on a single type which is not the case for any other set of traits I'm aware of.

@sfackler Sure there would still be magic. I've already aknowledged it (and this is certainly one of the reasons people are not necessarily confortable with rfc 1935 which actually proposes the magic trait Tuple). But this magic seems more natural to me than the current one with a set of rather specific rules for each primitive trait.

The ContainsNAN trait still seems like a really contrived example, though. What's an actual thing that you can to do but can't right now with auto traits?

To be honest I have no precise trait that I would like to implement and that I cannot. I just feel that current rules are too ad hoc. Once again every trait that can be automatically implemented by the compiler through the derive macro feels like a pragmatical hack. I believe most of them could be replaced by blanket implementations if one can access the inner details through a unified structure. And once specialization is ready this would still allow to implement specific logic when needed.

So my point is that it would be interesting to investigate such a system because:

  • it can be used to unify several mechanisms (impl T for .., restrictions on the Copy trait, #[derive]-traits);
  • it reduces cognitive load by providing a smaller and more consistent set of concepts to explain several aspects;
  • it encourages exploration and creativity by allowing people to create new "auto traits" that require information on the structure.

Automatically implementing traits that are currently derived would be a nightmare for API stability. Otherwise internal changes like switching a private field from a BTreeMap to a HashMap would break API compatibility since the automatic implementations of Ord and PartialOrd no longer exist, unless you remembered to specifically opt out of those impls. There is already a cognitive overhead to this being the case for Send and Sync - adding 10 more traits you have to worry about would be a mess.

Nothing prevents us from adopting the opposite behaviour for derived traits. You could also write a generic macro that can be added to your type to opt in the implementation (of PartialOrd for example) and which would rely on internal details. What would be the difference with the current situation? That this macro could be a usual macro in the standard lib rather than being a special rule encoded inside the compiler.

For a reason I don’t understand you seem very reluctant to explore this idea and to my opinion some of your arguments look like slippery slopes.

I’m confused - I thought the whole proposal here was around auto traits.

But more generally, there is a finite amount of resources that can be put into the language, both in terms of implementing stuff and thinking through complex new features and how they interact with the rest of the language. I like the motivation for complex additions to the compiler and language to be founded concretely in “here is a thing that I want to do but can’t/can do but it’s awful”. “It feels ad-hoc” does not meet that bar IMO.

I'm confused - I thought the whole proposal here was around auto traits.

Sorry, my explanations may not have been very clear. I first focused on auto traits because I didn't know the syntax impl T for .. had been added to handle Send and Sync. But as I understand it this feature has a quite limited expression power. That's where I started to mention that this and derived traits (and restrictions for Copy) may be handled by a common mechanism (primitive-type-family-auto traits like Tuple and Struct)

I like the motivation for complex additions to the compiler and language to be founded concretely in "here is a thing that I want to do but can't/can do but it's awful"

A need for custom derivable types has already been expressed.

"It feels ad-hoc" does not meet that bar IMO.

Independently from the existence of actual specific needs that this proposal may meet, I disagree with this argument. Having several ad-hoc mechanisms is certainly undesirable for a programming language and should be avoided as long as more generic alternatives exist. Failing to recognise general abstract patterns and only providing specific solutions to specific problem is the kind of approach that makes a language (and more generally a technical/scientifical device) very complicated. Of course abstraction is not the easiest path and sometime small steps and trial-and-error approach give you interesting hints about what will work and what will not. But this should certainly not be a reason to prevent us from looking for more general solutions. To my knowledge this is generally an attitude encouraged here and that's why I find the impl X for .. feature a bit unexpected and unsatisfying.