Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tracking issue for allowing overlapping implementations for marker trait #29864

Open
2 of 6 tasks
nikomatsakis opened this issue Nov 16, 2015 · 56 comments
Open
2 of 6 tasks
Labels
A-trait-system Area: Trait system B-RFC-implemented Blocker: Approved by a merged RFC and implemented. B-unstable Blocker: Implemented in the nightly compiler and unstable. C-tracking-issue Category: An issue tracking the progress of sth. like the implementation of an RFC F-marker_trait_attr `#![feature(marker_trait_attr)]` S-tracking-needs-to-bake Status: The implementation is "complete" but it needs time to bake. T-lang Relevant to the language team, which will review and decide on the PR/issue.

Comments

@nikomatsakis
Copy link
Contributor

nikomatsakis commented Nov 16, 2015

Tracking issue for rust-lang/rfcs#1268.

Status

Known bugs

History

Prior to stabilization

  1. Is it ok that adding items to a previously empty trait is a breaking change? Should we make declaring something a marker trait more explicit somehow? --> resolved by adding explicit #[marker] annotation, see Support an explicit annotation for marker traits #53693

Other notes

In #96766 we decided NOT to disable the orphan check for marker traits as part of this work (just the overlap check), which was a proposed extension.

@nikomatsakis nikomatsakis added B-unstable Blocker: Implemented in the nightly compiler and unstable. T-lang Relevant to the language team, which will review and decide on the PR/issue. labels Nov 18, 2015
@nikomatsakis
Copy link
Contributor Author

With respect to the second question of whether we should make a more explicit notion of marker trait, I have been pondering this since the RFC discussion. On the one hand, there are a number of places in Rust today where going from nothing to something is a breaking change: adding a private field to a struct, for example.

The difference here I think is that:

  1. Given the ability to specify defaults, adding an item to a trait does not necessarily have to be a breaking change otherwise (though methods and fns can cause new ambiguities etc).
  2. If there are impls that are taking advantage of the ability to overlap, there may just be no way to port older code forward, because it would require overlap.

However, the second point depends a lot on the fate of specialization. If we wind up adopting specialization, and in particular a model that does not require strict subsets, then it would still be possible to adapt existing impls. This makes me a lot less worried. Of course, implementing that form of specialization is tricky, but not (I don't think...) that much trickier than implementing THIS suggestion. The hard part in both cases is the potential for overlap.

sgrif added a commit to diesel-rs/diesel that referenced this issue Nov 25, 2015
There's a few limitations that I'm not happy with at the moment:

- The type of the expression has to be repeated twice
  (https://github.com/rust-lang/rust/isues/30056)
- It won't work with joins yet. I might be able to work around this in
  the short term, but it may require
  rust-lang/rust#29864 or specialization to
  express properly
sgrif added a commit to diesel-rs/diesel that referenced this issue Nov 26, 2015
This is useful for things like full text search, where you need to do an
expensive calculation to a parameter going into the where clause, but
doing it inline would repeat the calculation for each loop.

I had to expose a lot of internals here, because this is yet another
case where we can't properly express what we want without either
specialization or rust-lang/rust#29864. I
could not find a way to properly enforce the selectability of `Aliased`
when `with` was called more than once (this is ultimately the same
problem as joining multiple times). In the interim, we'll allow the
alias to be used anywhere.
@sgrif sgrif mentioned this issue Nov 28, 2015
57 tasks
@nrc nrc added the B-RFC-implemented Blocker: Approved by a merged RFC and implemented. label Aug 29, 2016
@sgrif
Copy link
Contributor

sgrif commented Nov 15, 2016

@nrc This has the RFC implemented label, but I don't think it has been. If it has, is there a feature gate that I'm missing? If it hasn't been implemented, is there anything I can do to help push this along? It's now been a year since the RFC was accepted.

@nikomatsakis nikomatsakis added B-RFC-approved Blocker: Approved by a merged RFC but not yet implemented. and removed B-RFC-implemented Blocker: Approved by a merged RFC and implemented. labels Nov 15, 2016
@nikomatsakis
Copy link
Contributor Author

@sgrif swapped labels

sgrif added a commit to diesel-rs/diesel that referenced this issue Dec 8, 2016
While this is useful for some cases where you don't want to load the
rows, this won't fill every use case for the expression, as right now
you wouldn't be able to build a query that references the outer table.
For us to do that and have it be type safe we'd need overlapping impls
for `SelectableExpression` (story of my life), which requires
rust-lang/rust#29864 being implemented.

Fixes #414.
sgrif added a commit to diesel-rs/diesel that referenced this issue Dec 8, 2016
While this is useful for some cases where you don't want to load the
rows, this won't fill every use case for the expression, as right now
you wouldn't be able to build a query that references the outer table.
For us to do that and have it be type safe we'd need overlapping impls
for `SelectableExpression` (story of my life), which requires
rust-lang/rust#29864 being implemented.

Fixes #414.
nikomatsakis pushed a commit to sgrif/rust that referenced this issue Apr 3, 2017
This patch allows overlap to occur between any two impls of a trait for
traits which have no associated items.

Several compile-fail tests around coherence had to be changed to add at
least one item to the trait they test against.

Ref rust-lang#29864
frewsxcv added a commit to frewsxcv/rust that referenced this issue Apr 4, 2017
…matsakis

Implement RFC 1268

This patch allows overlap to occur between any two impls of a trait for
traits which have no associated items.

Several compile-fail tests around coherence had to be changed to add at
least one item to the trait they test against.

Ref rust-lang#29864
@nikomatsakis
Copy link
Contributor Author

@rylev basically we just need to create a stabilization PR with a write-up that includes

  • quick summary of the feature
  • any changes since the RFC
  • some tests that highlight key aspects of the feature

There is some documentation here about writing a report and also preparing a PR.

@rylev
Copy link
Member

rylev commented Aug 19, 2020

I've tried to collect the known open questions and issues here.

Questions

  • Bike-shedding the name. Does this require an RFC? @nikomatsakis thoughts on this?
  • Is there any unsoundness around Unpin now that you are required to explicitly opt into marker traits with #[marker]? I haven't able to completely follow this topic yet, but it seems that @RalfJung was able to convince himself that there is no unsoundness although his latest comment does not indicate as such. I believe however that this concern along with others are resolved by the fact that the mechanism is now opt-in.
  • How does this interact with negative trait impls? The discussion of this seemed to have good ideas on how to handle this, but was consensus reached? This is tied to how #[marker] traits interact with the orphan rule. Currently #[marker] traits fully respect the orphan rule.

Bugs

I think most of these should be answered before we move forward with stabilization.

@RalfJung
Copy link
Member

I believe however that this concern along with others are resolved by the fact that the mechanism is now opt-in.

Yes, opt-in is key. Likely none of the existing auto traits can be made #[marker], but certainly not Unpin.

@rylev
Copy link
Member

rylev commented Sep 1, 2020

@nikomatsakis do you have thoughts on whether an RFC is required for stabilizing the name #[marker]?
@Centril brought up good concerns on negative trait impls, which was never fully resolved. How can we continue the discussion? Are Niko's ideas what we want to stabilize?

@nikomatsakis
Copy link
Contributor Author

@scottmcm did you have a specific reason to nominate this? :)

@nikomatsakis
Copy link
Contributor Author

cc @rust-lang/wg-traits -- I seem to recall some concerns about the implementation potentially being unsound? Does anyone remember what I'm talking about?

@nikomatsakis
Copy link
Contributor Author

We discussed this in our @rust-lang/lang meeting today. One thing we were wondering about are the active use cases and stakeholders -- seems like @rust-lang/wg-safe-transmute was one potential consumer, did the current design meet the needs there?

@scottmcm
Copy link
Member

Likely none of the existing auto traits can be made #[marker], but certainly not Unpin.

Can you elaborate on this one, @RalfJung? I would have guessed that Copy could be #[marker], for example. (Albeit I'm not sure how useful that one would be, given the extra magic requirements that exist on Copy.)

@RalfJung
Copy link
Member

For Unpin we have a concrete example where making it #[marker] would introduce soundness issues in a crate (#29864 (comment), taiki-e/pin-project#18).

I assume the same could happen, in principle, for any of the other existing marker traits, though I am not aware of concrete examples.

@lcnr
Copy link
Contributor

lcnr commented Nov 23, 2021

cc @rust-lang/wg-traits -- I seem to recall some concerns about the implementation potentially being unsound? Does anyone remember what I'm talking about?

I think the issue was #88139. The implementation now results in weird ambiguities, but at least it isn't unsound anymore afaik

For Unpin we have a concrete example where making it #[marker] would introduce soundness issues in a crate (#29864 (comment), taiki-e/pin-project#18).

Afaict the issue here is about impls for marker traits violating the orphan rules, which is generally unsound to my knowledge and not permitted by the current implementation. It might actually be possible to change all auto traits to marker traits again, only requiring opt-in for non-auto traits, as for those, adding an item to the trait definition would otherwise be a breaking change.

edit: nm, the idea is for a macro to add impl<T: NotImplemented> Unpin for NotUnpin<T> to prevent the type from implementing Unpin. This would not work anymore if Unpin was a marker trait. While I don't necessarily think that this pattern is the best solution and am hoping for the stabilization of negative impls for this instead, it is definitely a reason to keep marker traits as opt in.

@RalfJung
Copy link
Member

Afaict the issue here is about impls for marker traits violating the orphan rules, which is generally unsound to my knowledge and not permitted by the current implementation.

The entire point of the feature in this tracking issue is to allow violating the orphan rules by permitting overlapping implementations -- or am I misunderstanding?

@lcnr
Copy link
Contributor

lcnr commented Nov 23, 2021

it allows overlapping impls in the same crate, allowing it across crates is unsound without restricting the way marker traits can be used.

consider a crate like this:

#![feature(marker_trait_attr)]
#[marker]
pub trait Marker {}

pub struct B;

trait NotMarker {
    type Assoc;
}

impl NotMarker for B {
    type Assoc = usize;
}

impl<T: Marker> NotMarker for T {
    type Assoc = Box<String>;
}

if a different crate were to add an impl leading to B: Marker, we have an ambiguous associated type

@RalfJung
Copy link
Member

Oh, because rustc actually uses negative reasoning inside a crate, so not all 'monotone' extensions of the impl set are actually allowed... that's nasty.

@scottmcm scottmcm removed the I-lang-nominated Nominated for discussion during a lang team meeting. label Dec 6, 2021
@joshtriplett joshtriplett added the S-tracking-needs-to-bake Status: The implementation is "complete" but it needs time to bake. label Jan 19, 2022
@nikomatsakis
Copy link
Contributor Author

I have an action item to leave a comment here but I can't find the notes. In any case, my vague recollection is that:

Despite marker traits permitting one to stretch the coherence rules, they are specifically targeting the overlap rules and not the orphan rules (i.e., they don't let you write impls in crates that you wouldn't have been permitted to write an impl in). If we ensure that marker traits follow the same orphan rules as everyone else, I don't think they pose any particular problem for the negative reasoning that we do elsewhere.

There was also a (related) interaction between marker traits and negative impls that was discussed here. In short, the problem was that if we lifted the orphan rules for marker traits, then one could add an explicit !Marker impl in a crate and an Marker impl in another crate. As I noted then, the way I think about it is that, after a trait is published, only one crate ever has the right to add any particular impl (positive or negative). Specifically, if an impl applies to a type X, only the crate in which X is declared has the right to add that impl. This fits with the fact that adding blanket impls (which could apply to such types) is a semver-breaking change after a trait is released. This same idea applies to negative impls too: you can only add a negative impl if you would've been the one to add the positive impl.

The case that @RalfJung is talking about seems to be the same, but the "negative reasoning" in question is implicit due to the fact that everything is within the same crate.

@SamPruden
Copy link

SamPruden commented May 18, 2022

One use for this which I'm particularly excited about but haven't seen explicitly mentioned is that it allows things like this:

#[marker]
trait TupleHas<T> {}

impl<T1> TupleHas<T1> for (T1, ) {}
impl<T1, T2> TupleHas<T1> for (T1, T2) {}
impl<T1, T2> TupleHas<T2> for (T1, T2) {}
impl<T1, T2, T3> TupleHas<T1> for (T1, T2, T3) {}
impl<T1, T2, T3> TupleHas<T2> for (T1, T2, T3) {}
impl<T1, T2, T3> TupleHas<T3> for (T1, T2, T3) {}
// ...

A use case for this would be Bevy's queries, which look like this.

fn score_system(mut query: Query<(&Player, &mut Score)>) {
    // Type safe access to components
    for (player, mut score) in query.iter_mut() {
        // Some score updating logic here
    }

    // However, we can also do this:
    let player: &Player = query.get_component(some_specific_entity);
    // We're only allowed to call get_component with one of the types in the Query.
    // (Player or Score in this case). 
    // Currently, this is only enforced with a runtime error.
    // A QueryHasReadAccess<T> marker trait could allow this to be a type error instead. 
}

I'd love to see this stabilized!

@scottmcm
Copy link
Member

@SamPruden I'd also like to see this stabilize.

It looks like there's at least one open issue that probably blocks it, though: F-marker_trait_attr `#![feature(marker_trait_attr)]`

@SludgePhD
Copy link

I'm currently writing a linear algebra library that would really benefit from this feature as well, so I'd love to see this move forward.

My usecases, in case they're insightful:

The first is VectorLike, a trait implemented when one dimension of a matrix is statically known to be 1. This allows interpreting the matrix like a (row or column) vector and allows using some methods and operators where it would otherwise be ambiguous whether they operate in column-major or row-major order (.iter(), indexing with a scalar, etc.).

#[marker]
trait VectorLike {}
impl<D> VectorLike for (Const<1>, D) {}
impl<D> VectorLike for (D, Const<1>) {}

The second is DimEq, a trait implemented when two matrix dimensions are potentially equal at runtime. This is useful in the presence of runtime-dynamic matrix dimensions, indicated by the Dyn type – those require a runtime check in addition to the DimEq bound.

#[marker]
trait DimEq {}
impl<const D: usize> DimEq for (Const<D>, Const<D>) {} // guaranteed to be equal
impl<const D: usize> DimEq for (Dyn, Const<D>) {}  // *can* be equal (ensured by runtime assertion)
impl DimEq for (Dyn, Dyn) {}  // *can* be equal (ensured by runtime assertion)
impl<T, U> DimEq for (T, U) where (U, T): DimEq {} // order shouldn't matter when using this
// (important in generic code calling other generic code, otherwise they have to require both or agree on order)

// Notably, this isn't implemented for `(Const<A>, Const<B>)` when A and B are different.
// catches mistakes at compiletime instead of runtime when possible!

@Kolsky
Copy link

Kolsky commented May 4, 2024

@SludgePhD I wonder if your logic for

impl<T, U> DimEq for (T, U) where (U, T): DimEq {}

is right. Notably, it overflows the current typechecker, and the next trait solver intends to resolves cycles with a "yes" answer by separating is-impl and has-impl. It would work as if you've additionaly written

impl<T> DimEq for (T, T) where (T, T): DimEq {} // has impl, so is impl; in other words, always holds.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-trait-system Area: Trait system B-RFC-implemented Blocker: Approved by a merged RFC and implemented. B-unstable Blocker: Implemented in the nightly compiler and unstable. C-tracking-issue Category: An issue tracking the progress of sth. like the implementation of an RFC F-marker_trait_attr `#![feature(marker_trait_attr)]` S-tracking-needs-to-bake Status: The implementation is "complete" but it needs time to bake. T-lang Relevant to the language team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests