-
Notifications
You must be signed in to change notification settings - Fork 13k
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
implement new object safety rules #17704
Conversation
I'm increasingly concerned about this from a library design view point. It requires defining and importing a whole pile of extra traits, and also requires a library author to remember and think about "enabling" trait objects for a trait. E.g. Most libraries will have less associated code than our stdlib and compiler, and so even less "chance" for a trait object to be made and hence a lower chance of detecting the problem locally. |
I agree with @huonw, and issues like #17701 worry me a great deal if that's what we're forced to modify to go down this route. I know of some current uses of boxed iterators that basically have no replacement if this lands. Are there possibly alternative solutions to rust-lang/rfcs#255 which don't involve this splitting up of traits? |
// `enforce_object_limitations()` if the method refers | ||
// to the `Self` type. Substituting ty_err here allows | ||
// compiler to soldier on. | ||
// It is illegal to create a trait object with methods which incclude |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
incclude -> include
I share @huonw and @alexcrichton's concerns about the impact of this on library APIs. In particular, @huonw's point that this
is a very good one that was not discussed at the meeting where we accepted this RFC. And as we're seeing with the changes needed for I think this RFC merits another round of discussion before we land these changes, where we think through the concrete implications on That said, it's interesting to consider the design of (On the other hand, in the future these adapter types could be provided as associated types via HKT, and then you could specialize the implementations for performance reasons...) |
I'm happy to discuss it more, but I still think this is the right approach. I think it's a GOOD thing that trait authors have to think about enabling traits for objects at trait design time. Specifically, if you have some trait X that is not object safe, then it means that if we permit objects to be created they are second-class citizens: many of the methods of the trait are unavailable, and you cannot use them with generic functions (because the type X cannot implement the trait X). Iterator makes for a good case study. The fact that we could create objects of type Iterator means that we think the current design is object-friendly, but actually it's quite hostile: you can call
then I can't use my It would be better if we made the core iterator trait object-safe and then implemented |
Well, let me rephrase. I don't think it's "GOOD" that we don't detect failures until you try to use as an object. But I worry very much about composability -- and I don't like the idea of second-class objects floating around that have some but not all of the capabilities of their trait. |
Just to make sure my point comes through: It's true that you don't detect the fact that a trait is not object-safe until you try to make an object, but in the current design, it's even worse: you don't detect the problems around trait objects until you actually try to use them in a non-trivial way. (Ok, last comment, I promise) |
@nikomatsakis I had the same thought about iterators, though in the long run it would be nice to be able to override adapter implementations when you can do something smarter for your particular iterator (though that requires HKT, etc.) Splitting up the trait will force the issue. I take your point about needing to think about these issues when designing an API, but the worry is: what happens when some API you don't control fails to do so? I suppose you can always work around it by creating your own trait, with just the object-callable methods, and a blanket impl that just calls out to the existing trait. |
Actually, a follow-up for iterators: Suppose we went with the design where you can create trait objects for any trait, but that you only get DST-style impls for certain traits. We would not get the automatic DST-style impl for Another, somewhat related worry: fully automatic DST-style |
@aturon Yes, I realize it's a tradeoff. Clearly this is a point about which reasonable people can disagree. I think I'd prefer to make the distinction sharper precisely because I think it's easily forgotten, so the more we gray the line the more people get confused. I think it's clear from this conversation that it's very easy to not realize the repercussions of generic methods in a trait and so forth. (It almost makes me wonder if it's worth having a different declaration form or some other way to opt-in to advanced features.) It's worth considering what the workarounds are in both cases. Under this design, end users who want an object but can't have one have to make a copy of the trait that deletes the disallowed methods. This is annoying, but has the advantage that its limitations are clear: naturally you won't be able to the disallowed methods on the new trait, since they're not there, and naturally you won't be able to use the trait elsewhere. Under the other design, the workaround is... well, I don't know what it is. I guess we could permit you to hand-write an impl that failed if you tried to use the negative methods. Regarding overriding |
@aturon I guess that my specialized-based proposal and your |
I don't trait objects are as common as we are imagining and therefore this is not as big a problem as it looks: e.g., there is not a single Iterator object in all the libraries and compiler. And in all that code there were only 3(?) traits which broke with this change. And I agree with Niko, in those cases, the split caused a better design - extracting the We discussed @aturon's suggestion, the problem is that having T not implement T for some traits seems even worse than having T not implement T all the time - I would hate to have to explain that to someone. Whereas the object-safety rules are pretty easy to explain. |
I have to run atm, but to argue devil's advocate: it is true that even if
it is STILL not usable with objects by default, it'd have to be:
This weakens my argument considerably. :) (Though I have been privately wondering if implied bounds could allow us to remove the |
One of the key points here to me is that the author may not actually be the one who's actually thinking about the trait-object-ness of the trait. I think that the base of idiomatic Rust code is expanding quickly beyond the libraries and compiler, so I'm not sure how much longer we can use that as our metric to determine the impact of a change. Here it's easy to say that |
@alexcrichton when that is true we are in a library/client situation and we can expect library designers to think of this sort of thing, its just another part of library design. We are pre-1.0, we can break downstream code as long as there is a good work around, which I think here there is. Do you know if iterator objects are common in downstream code? It seems a bad idea to conjecture here. |
I only personally know of the one example I linked to above, I'm unaware if there are more instances. |
@alexcrichton In the future, that use of pub trait Headers {
// Theoretical
pub type HeaderEntries<'a>: Iterator<(&'a str, Vec<&'a str>)>;
fn find<'a>(&'a self, key: &str) -> Option<Vec<&'a str>>;
fn has(&self, key: &str) -> bool;
fn iter<'a>(&'a self) -> HeaderEntries<'a>;
} That said, we can't do it today, which is critical since there is no promise of when if ever HKT will be added, and it seems it will almost certainly be after 1.0. In my experience with downstream Rust code, For instance, in Iron, we've got: pub trait Chain: Handler {
fn new<H: Handler>(H) -> Self;
fn link<B, A>(&mut self, (B, A)) where A: AfterMiddleware, B: BeforeMiddleware;
fn link_before<B>(&mut self, B) where B: BeforeMiddleware;
fn link_after<A>(&mut self, A) where A: AfterMiddleware;
fn around<A>(&mut self, A) where A: AroundMiddleware;
} which is not object safe, but it is still useful to make Generally, it's sometimes useful to have methods which you can only call through |
@reem in that case, it seems that you already have two traits -- why would you not make a |
Oh, I see, I misunderstood. You said you may want to add more methods in the future. |
We discussed and approved this in today's meeting. I just rebased it. |
@@ -942,6 +934,20 @@ pub trait Reader { | |||
} | |||
} | |||
|
|||
/// A reader which can be converted to bytes. | |||
pub trait BytesReader: Reader { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you instead provide a blanket impl
of this trait for all Reader
s?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(That would avoid this being a breaking change.)
Thanks @nick29581 for adding the blanket It occurs to me that if we want to fully avoid breakage, we need to add these new traits to the prelude, which is fine by me. @alexcrichton is that ok with you? |
@nick29581 I talked with @alexcrichton about this on IRC, and his feeling is that he'd rather stomach a bit of breakage than further pollute the prelude (we're not sure whether So this is fine as-is. Can you adjust the commit message that includes the |
r+ modulo new test |
80d28d2
to
b882d35
Compare
closes rust-lang#17670 [breaking-change] Traits must be object-safe if they are to be used in trait objects. This might require splitting a trait into object-safe and non-object-safe parts. Some standard library traits in std::io have been split - Reader has new traits BytesReader (for the bytes method) and AsRefReader (for by_ref), Writer has new trait AsRefWriter (for by_ref). All these new traits have blanket impls, so any type which implements Reader or Writer (respectively) will have an implmentation of the new traits. To fix your code, you just need to `use` the new trait.
… r=Veykril fix: let glob imports override other globs' visibility Follow up to rust-lang#14930 Fixes rust-lang#11858 Fixes rust-lang#14902 Fixes rust-lang#17704 I haven't reworked the code here at all - I don't feel confident in the codebase to do so - just rebased it onto the current main branch and fixed conflicts. I'm not _entirely_ sure I understand the structure of the `check` function in `crates/hir-def/src/nameres` tests. I think the change to the test expectation from rust-lang#14930 is correct, marking the `crate::reexport::inner` imports with `i`, and I understand it to mean there's a specific token in the import that we can match it to (in this case, `Trait`, `function` and `makro` of `pub use crate::defs::{Trait, function, makro};` respectively), but I had some trouble understanding the meaning of the different parts of `PerNs` to be sure. Does this make sense? I tested building and using RA locally with `cargo xtask install` and after this change the documentation for `arrow_array::ArrowPrimitiveType` seems to be picked up correctly!
r? @nikomatsakis