Collective bikeshed of `#[marker] trait Foo { .. }`

Background

In RFC 1268 we introduced a notion that traits with zero items in them can be implemented many times and permit overlap because they lack computational content and therefore it is not a violation of coherence (also see Quantified class constraints, section 3.4, with pdf).

Later, concerns were noted that the mechanism proposed in the was automatic rather than opt-in:

@cramertj Agreed; the discussion seems to have decided that this needs to be opt-in on the trait, not something that just happens. I've started a PR towards doing so: #53693 - @scottmcm

It was suggested that we should use an opt-in mechanism with an attribute #[marker]. For example, you would write:

#[marker] pub trait Copy: Clone {}

This change was then implemented.

However, with respect to the naming of the attribute, concerns have been raised that the name is sub-optimal. In particular, the attribute name does not suggest what effect it has on the trait (permitting overlap). Furthermore, if we permitted defaulted items which are not overridable, with for example:

#[marker] unsafe trait SafeToZero: Sized {
    fn zeroed() -> Self { unsafe { MaybeUninit::zeroed() } }
}

then SafeToZero would no longer fit the description of being a marker trait.

The brainstorm

Me and @scottmcm have already done some bikeshedding over at Discord and we came up with the following list of possible names:

  • #[incoherent] -- this is not really appropriate because it is inaccurate per the meaning of coherence and it also sounds quite bad.

  • #[overlappable] -- this naming paints an accurate description, but misses the fact that the implementations can overlap and not the trait itself. Perhaps that is hard to capture? The attribute name is also twice as long (12 characters) as marker (6 characters). We do not tend to use the -able suffix on traits; but we do have instances of paths with -able in the name. For example: Peekable and unreachable neither of which are traits.

  • #[overlap] -- this is shorter and based on the same root word, but it isn't an adjective.

  • #[mixin] -- this is possibly vague and also perhaps a synonym of trait?

  • #[permit_impl_overlap] -- these names are possibly too long but they are quite descriptive as to the effect.

  • #[allow_impl_overlap] -- the word allow might have more precedent here due to #[allow(..)] but perhaps the connotation with lints is not something we wish to impart here.

  • #[impl_overlap] -- Here we simply omit "allow" and leave it inferred; the main benefit of doing so is brevity which matters if the attribute is used a lot (and we don't really know if it will or won't...).

  • #[allow_overlapping_impls] -- this is just a rewording that may read better?

  • #[permit_overlap] -- this one doesn't communicate about implementations but does state explicitly that we are permitting something.

  • #[fact] -- the idea is that we are stating a fact, but this may not work so well because all trait implementations are really proof witnesses about a (type, trait).

  • #[multiple_impls] -- this states clearly that multiple implementations are allowed, but it leaves out the important qualifier that multiple impls are allowed for the same type which might not be understood as people may think multiple impls of the same trait is referred to.

The collective bikeshed

In the previous section I've outlined some names that we thought of; some of these are not serious contenders and are merely provided for completeness and to avoid repetition of dead ends. Other names are more serious; for example, I think that #[overlappable] is not too shabby.

Finally, we would like your help in determining the best name.

  • What do you think about the various names above?
  • Do you have any preference?
  • Are there names which we have yet to consider and what is the rationale for those?
  • Should we use an attribute for this at all, or should this be a contextual keyword?

Let the bikeshed begin =P

#[marker] is OK IMHO, since thatā€™s already part of Rust terminology. edit: there are good counter arguments in this thread

1 Like

One more option for the brainstorm: marker is fine because itā€™s already a term of art (see std::marker), and having methods on the trait instead of as free functions doesnā€™t change its marker-ness.

For example, if we had #[marker] trait Sized { const SIZE_IN_BYTES: usize = magic(); }, it would still be a marker trait. And Copy is already in marker, despite having associated logic (albeit via Clone, not directly).

Edit: Oh, @kornel beat me to it

3 Likes

About #[marker]ā€¦ I do think that marker entails a trait with zero items; this is noted in @killercupā€™s blog post Trait Driven Development, by @nikomatsakis here and it is a true statement about every trait in std::marker (well, Copy implies Clone but that is indirect as previously noted).

Furthermore, if you have a trait Foo {} which has zero items (and so it will be seen as a marker traitā€¦) but without #[marker], then all marker traits are not #[marker] traits which results in a somewhat confusing situation.

Finally I think that what I described above wrt. #[marker] not telling the reader what effect it has on the trait is somewhat of a problem. The effect of the attribute is really all about overlap and coincidentally it is sound for there to be overlap with traits that have zero items (marker traits) or traits with provided definitions that canā€™t be overridden (not marker traits, I think). However, one could perhaps (and I want to stress that this is quite hypothetical) imagine a broader system of traits where it could work. Naming it #[marker] would make that difficult.

Why do I want traits that have multiple impls, that can overlap? What is my goal, as a programmer, that this lets me achieve?

I have no idea from the above, but perhaps there might be a better name lurking in a clear articulation of that?

2 Likes

See the RFC's text, and asssociated discussion.

I think it would allow crate C to implement marker-trait b defined in crate B on type a defined in crate A without having to worry about coherence issues with where else trait b might be implemented for type a. Now, thatā€™s all very abstract, so maybe something like this will get you where you want to be:

Crate: cheap-clone

#[marker] pub Trait CheapClone : Clone {};

pub impl CheapClone for T : Copy ();

pub fn must_be_fast ( obj : &T  ) -> T where T : CheapClone
{
   // This function must execute fast, but, doesn't want to only support copy types. It also wants to support "cheaply" (for some definition of that) cloneable types
  // do some stuff
  obj.clone()
}

Now, I want to use this library with 3rd party types that I want to mark as ā€œCheaply Cloneableā€ so I can call this function etc. Because ā€œCheaplyCloneā€ is a ā€œMarker Traitā€ (and designated as such), Iā€™m permitted to do that even though, normally, a 3rd party crate is not permitted to provide implā€™s for a Trait.

1 Like

Perhaps instead of leading with the verb "allow" instead invert the order?:

  • #[impl_overlap_allowed]
  • #[overlapping_impl_allowed] (I prefer the above mor than this)
  • #[overlap_allowed] (still prefer the first)

Yeah, I really like the first of the bullet points above. I think it capture what seems like your best/preferred option, but, in a way that is less suggestive of an association with lints.

Firstly - it's entirely possible there isn't a clear "everyday" definition that isn't heavily-laden with type-theoretical terminology. Since the goal is to find a clearer and better name for general users who haven't absorbed or retained all that theory, I thought it might be worth making sure.

Thankyou. I suspected it was somehow connected to this.

The term of art in the documentation here is "the orphan rule", so perhaps a name connected with that term or concept would help - that the trait should not be considered an orphan for the purposes of that rule when implemented from another crate.

Or perhaps we should adjust that terminology to match whatever we come up with here ("the overlap rule"?), since some of those potential terms might be considered sensitive.

1 Like

I've never quite been able to understand why it is referred to as "the orphan rule". I believe I understand the issues, but, I just don't understand the "orphan" metaphor for it. But, to be honest, I haven't studied/pondered it that much, so it's probably my ignorance.

EDIT: When I hear "Orphan" I think, "OK, who is/was the parents and how/when did they die?"

1 Like

To more directly answer your questions:

All of them based around "overlap" seem the best. As I mentioned above, I kind of like the "allow" also, but, inverted order so as to disassociate from lints a little.

I'd say, from those you suggested, #[allow_impl_overlap] (but, see my suggestions below).

One possibility I can think of is to emphasize the notion of "No Run-time Computational Dependency" (hmmm...am I saying that right?). That is a word/phrase that says, "Implementations of this Trait will have no meaningful computational complexity that could possibly differentiate alternative implementations for the same struct/type and it is guaranteed to remain so, so, you are free to implement this trait on 3rd-party structs." - What a mouthful! Something like:

  • #[no_computation]
  • #[no_compute] (prefer the above)
  • #[simple] (maybe? kind of a superset of 'marker'?)
  • #[basic] (similar to 'simple', but, I prefer 'simple')
  • #[property] (again, like 'simple', this is a just a 'property' trait, a super-set of 'marker' trait, but, not a full trait...I don't like this as much I think)

I think something like #[simple] sounds pretty good and can be explained as a hierarchy of complexity for traits where: Trait > Simple Trait > Marker Trait. A "marker trait" is a trait with no methods at all. A "simple trait" is a trait that may only have "Non-Computational Content" methods. A "Trait" can have methods of any kind. Perhaps there are other "Levels" of trait yet to be defined as well.

Maybe. Something like:

pub simple trait Foo {};

That being said, Rust seems to want to eschew new keywords in favor of using attributes. I see no reason in this case that a contextual keyword is especially warranted. Especially if, as I mentioned above, we think in terms of a hierarchy of Trait kinds that have different sub-sets of behavior/functionality/effect. In that case, using attributes seems really appropriate to me. Also, because adding the attribute doesn't make the struct eligible for 3rd party implementation, it just notes that the API developer is acknowledging that it is eligible based on it's defined properties and that the API developer is making some guarantee for purposes of API stability to not modify the definition of the Trait to change that fact in the future.

I do like the name #[marker], but another reasonable alternative might be #[trivial], since an impl of Trait for T is simply an exists-relation, as opposed to one that holds any information - alternatively put,

āˆ€T āˆˆ Type, āˆ€t, u āˆˆ Trait(T), t = u

6 Likes

What about #[empty] or something similar because it is a guarantee the trait contains zero items? Could such a trait also be used as an additional constraint like dyn Trait + Empty?

I like the double meaning of trivial.

  • Mathematically: The impls are all identical because theyā€™re trivial.
  • Colloquially: What about overlap? Itā€™s trivial, who cares?!
4 Likes

This one seems pretty nice. Not too long and to the point. The only missing bit is impl, but that can be inferred?

That would work as a description of the trait, but it is redundant because the user can see that the trait contains zero items from the definition. It also leads to questions about why some trait Foo {} is not #[empty] and why the annotation is necessary at all. If we also allow items that can't be overridden in the trait, then #[empty] is no longer an apt description.

I like where this is heading I think, #[trivial] is in the same vein but I think I prefer #[trivial] out of these choices.

It does seem nice and I think it extends well to traits with non-overridable defaulted items.

I guess the question is if users will understand the implication wrt. overlap? I think unless we pick something like overlapping_impls_allowed then some users will have to look up the meaning of the attribute anyways (even if we have #[overlapping] so this may not be a problem in practice?

I think the innate quality that matters is that all implementations of the trait are known to be equivalent. The fact that this allows overlap falls out of this. The #[trivial]-ness of a trait is the property of the trait, and the fact that overlapping is possible is a result of that.

Annotating the result seems backwards. Annotating the property seems more right.

For the purpose of discussion, how (does?) this property interact with auto traits? I donā€™t think it influences the name, but it might influence the choice of attribute versus keyword.

I guess itā€™s a matter of how intrinsic the property is.

Of the other attributes implemented, which are breaking changes to add/remove from a type? The only one I know (excl #[derive]) is that #[non_exhaustive] is fine to remove and breaking to add. I assume #[trivial] would be fine to add and breaking to remove.

5 Likes

I defer to @scottmcm on this since they last touched the implementation.

Yes, I think that's accurate.

I'd definitely prefer one of the names with "overlap" in it.

  • all the alternative names like marker/empty/trivial/simple/basic all fail to convey to the reader what is actually being enabled by the attribute

  • as @Centril pointed out, those names create an awkward "not all empty traits are #[empty] traits" situation

  • the concept of "overlapping" impls is something you already need to understand for the orphan rule and specialization and so on.

This argument doesn't really work for me because overlapping is an opt-in feature. If allowing overlap was automatic and this attribute was about, say, making a public API commitment to never make the trait non-trivial so that client crates could also write overlapping impls, then this would be more persuasive.


As for which overlap-using name, I have no strong opinions, but I do want to throw out the additional paint color of #[impls_may_overlap] since I think there's some precedent for using may like this.


I always assumed the intended metaphor is that the type and trait are the "parents" of the impl, because an orphan impl is (very roughly) an impl where both "parents" are far away/in some other crate.

Then again, it's not like the type and trait created the impl then went to another crate to abandon it there before going back to their home crate(s). It's the other crate author that created the impl out of thin air without either parent's involvement, so I guess that's more like a "test tube baby impl"? Oh well.

2 Likes

auto traits are not implicitly #[marker], but can be. Positive and negative impls still conflict:

error[E0119]: conflicting implementations of trait `Foo` for type `u32`:
 --> src/lib.rs:7:1
  |
6 | impl<T:Copy> Foo for T {} 
  | ---------------------- first implementation here
7 | impl !Foo for u32 {}
  | ^^^^^^^^^^^^^^^^^ conflicting implementation for `u32`

#[impls_may_overlap] is very appropriate, since in English "may" historically has been used to convey permission.

1 Like