Join GitHub today
GitHub is home to over 31 million developers working together to host and review code, manage projects, and build software together.
Sign upRFC: Hidden trait implementations #2529
Conversation
Centril
added some commits
Jun 24, 2018
Centril
added
the
T-lang
label
Aug 24, 2018
Centril
self-assigned this
Aug 24, 2018
scottmcm
reviewed
Aug 24, 2018
text/0000-hidden-impls.md Outdated
This comment has been minimized.
This comment has been minimized.
|
How does this interact with blanket impls? Like suppose it were Edit: A better description: for a concrete |
This comment has been minimized.
This comment has been minimized.
The interaction is described here. While this may seem weird, I think it is also useful in some cases. Consider a conditional and hidden blanket impl like However, I don't think this sort of intentional prevention will be the main use for this. It is something that you can do, but the main use case is to allow users to implement traits they actually need to use for internal purposes but to not expose implementations and avoid guaranteeing them.
That depends on what you mean by see here. The error message in the link above is: error[E0119]: conflicting implementations of trait `crate_A::Property` for type `Thing`:
--> src/main.rs:<line>:<column>
|
L | impl Property<Thing> for Thing {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: conflicting implementation in crate `A`:
- crate impl<T> crate_A::Property<T> for T { ... }
note: the implementation exists, but is hidden.Note in particular that the author of crate B can see that there is a hidden implementation in crate A that is in the way. This enables B's author to contact A's author and see if they can perhaps work something out. As a final note, the reason why E0119 must be emitted is that we must preserve coherence because to violate coherence would be unsound and cause breaking changes to unsafe code which may assume coherence as an invariant. The canonical example of this is the hash table problem. |
This comment has been minimized.
This comment has been minimized.
|
I think this RFC needs a section about how it interacts with specialisation, consider this: // crate A
trait Property {
fn get(&self) -> u32;
}
impl<T> Property for T {
default fn get(&self) -> u32 { 42 }
}
crate impl Property for Thing {
fn get(&self) -> u32 { 4242 }
}Crate |
This comment has been minimized.
This comment has been minimized.
|
@TimNN Nicely spotted! I hadn't considered that interaction, so thank you for raising this. :)
If by "see" we mean "use", then if this were allowed to happen, then it would cause the type system to be unsound because it would be incoherent (i.e: allow different dynamic semantics depending on where the implementation was resolved in). So I see two principal ways to deal with this:
What do you think? |
This comment has been minimized.
This comment has been minimized.
(2) would be my preference as well (also because IMO it's simpler to understand than option (1)). |
This comment has been minimized.
This comment has been minimized.
|
@TimNN Yeah that's a great point! In fact, it felt heavier to write the explanation for 1) so I wrote the one for 2) first =P. The explanation for 2) is also more obvious I think since you can fit it into an error message. |
This comment has been minimized.
This comment has been minimized.
burdges
commented
Aug 26, 2018
•
|
I'm kinda dubious about the motivation here: Are we trying to expose I suspect this would usually be best addressed with hidden types and simplified delegation, but given that delegation has languished so long.. I'm wondering if some lint scheme might achieve this better, so the impls would technically be exposed, but any user outside the current crate gets a warning specified at trait declaration time.
I'm thinking lints prevent crate authors from really depend on hiding impls, so they'll be more conservative, while simultaneously admitting a wider range of warnings:
It just seems like hiding is too blunt an instrument and people who want this should explain when the impl is or is not usable. A lint could permit hiding generic impls that exist for whatever reason, but would probably not hiding anything automatically, instead the hider should write out lints for each problematic trait. |
This comment has been minimized.
This comment has been minimized.
In the case of
Other examples, as discussed in the motivation, include not wanting to expose
This is discussed in depth in the section on alternatives. Remember that you both have to 1) define the newtype struct, 2) wrap (and sometimes unwrap) the base type in the newtype every time you need the crate-local-only implementations. 3) create the delegating implementations. Thus, comparatively speaking, using forms of GeneralizedNewtypeDeriving or delegation per #2393 requires a considerably larger, relatively speaking, annotation burden as compared to just adding Also consider that delegation or newtype-deriving does not generally work and can be quite restrictive. For example, if we take a look at RFC #2393, it does not work at the moment for: associated types, associated consts, All in all, I believe using the newtype approach is an indirect approach that does not highlight the author's intent and that adds a non-trivial amount of extra thinking and reading burden on the author. Thus, I think it is a worse solution for readability, maintainability, and for writing ergonomics.
I will add a discussion about this in the section on alternatives. However, when dealing with Furthermore, it is not clear how this I would also like to add that comparatively speaking, As for wanting to provide more tailored error messages, we could expose better facilities for custom error messages in general, and hidden implementations could be part of that. For example, you might have some sort of However, I think that most of the time, you would just like to hide an implementation without explaining yourself. In the cases where I have needed this, that applied. If I had to provide an explanation, there is a chance that it would have been too heavy a burden wherefore I might have accidentally publicized the implementation instead. |
This comment has been minimized.
This comment has been minimized.
burdges
commented
Aug 26, 2018
|
It sounds like visibility modifiers for impls might not adhere to the same rules as visibility modifiers for inherent items or non-public traits though? Are you sure it's wise to permit hiding all traits? Can I hide only a supertrait? It sounds strange to hide How does hiding interact with generic impls? If I hide It's tempting to keep hidden impls in line with existing hidden traits here. That results in incomplete hiding, but incomplete hiding is what you want at least half the time anyways, and incomplete hiding is at least fixable with more visibility modifiers, ala specialization. Can you forbid hiding an impl for your own tait via syntaxes enabled by specialization, amybe I do think lints provide answers to most of these questions, namely the burden is on the hider for both coverage and information. It's impossible for these answers to always be correct though because the space is more complex than with existing visibility modifiers. |
This comment has been minimized.
This comment has been minimized.
In what sense? The differences where they exist are specified in the reference except for the interaction with specialization which I've proposed a rule for in the above discussion.
Rules for auto traits are specified in point 8. pub(self) impl !Send for MyUnsafeType {}As for Can you elaborate on
No. This is specified in 4. #[derive(crate Clone, pub Copy)]
struct Foo;but you may write: #[derive(pub Clone, crate Copy)]
// equivalently:
#[derive(Clone, crate Copy)]
struct Foo;
You can't hide a bound. You can hide the fact that some type
If If you believe there are faults in the technical specification of this RFC, I invite you to leave line comments where appropriate so that I may fix those faults.
Elaborate? I have discussed the situation wrt. blanket impls here: #2529 (comment).
I don't believe in the linting approach because fundamentally, lints are not about encapsulation and API abstraction. We use visibility modifiers for that to enforce properties, so it seems to me natural to use |
This comment has been minimized.
This comment has been minimized.
burdges
commented
Aug 26, 2018
|
I'd missed that hidden bounds were forbidden. That makes this less complex and much less useful, which sounds good. I do still think hidden impls should always be justified in comments. As otherwise downstream could reasonably just fork the upstream under the assumption that upstream was being overly shy about its public API. Clippy could maybe complain if a hidden impl lacked a comment? |
This comment has been minimized.
This comment has been minimized.
Sure; it could check for a doc comment. |
Centril
added some commits
Aug 28, 2018
This comment has been minimized.
This comment has been minimized.
|
Update:
|
This comment has been minimized.
This comment has been minimized.
|
How do you handle "excluded middle" reasoning using specialization? I.e., suppose that someone builds a crate #![feature(specialization)]
use std::fmt::Debug;
#[derive(Debug, Default)]
pub struct True;
#[derive(Debug, Default)]
pub struct False;
pub trait IsCopy {
type IsCopy: Debug + Default;
}
impl<T: ?Sized> IsCopy for T {
default type IsCopy = False;
}
impl<T: ?Sized + Copy> IsCopy for T {
type IsCopy = True;
}Then what is This can be done even with what I think is @aturon's newest specialization RFC: http://aturon.github.io/2018/04/05/sound-specialization/ A sound solution would be that the |
This comment has been minimized.
This comment has been minimized.
|
I haven't really kept up with this thread, but my general feeling is that there are a lot of potential minefields when playing with coherence and versioning. Previous RFCs like specialization have already demonstrated that there is a lot of subtly. I would rather see currently accepted RFCs about the trait system implemented before we add more features. |
This comment has been minimized.
This comment has been minimized.
|
@mark-i-m The purposes of these discussions are precisely to surface such subtleties and either determine that they are unsolvable, and thus nix the proposal, or to try to solve them collectively. It's better that we surface potential conflicts and problems in interactions in various RFCs and ideas before it is too late. @arielb1 So to elaborate a bit on your snippet, we could run into problems such as with the following? // crate alpha
pub trait Pred {}
pub trait Fun {
type Out;
}
impl<T> Fun for T {
default type Out = Box<u8>;
}
impl<T> Fun for T where specialize(T: Pred) {
type Out = [usize; 2];
}
pub fn use_fun<T: Fun>(x: <T as Fun>::Out) {
// do stuff...
// alpha thinks that <Foo as Fun>::Out == Box<u8>
// and will run destructor causing a free(..) on [usize; 2]
// which will segfault.
}
// crate beta
struct Foo;
crate impl alpha::Pred for Foo {}
fn use_foo() {
let _: <Foo as alpha::Fun>::Out = [0, 1];
}
fn use_fun_at_distance() {
alpha::use_fun([0, 1]);
}I don't know how to solve this as readily as the other problem I closed wrt. specialization.
Yeah; I agree.
Can you talk a bit more about "crate at infinity"? With respect to confusion I think that started with |
This comment has been minimized.
This comment has been minimized.
@Centril I completely agree, and I think that we should try to surface those nuances. I was merely expressing my current stance that perhaps this proposal may be ahead of its time. |
This comment has been minimized.
This comment has been minimized.
That's just a figurative way of talking about an imaginary crate that depends on all of the relevant Rust crates, and nothing depends on it. This means that all "non-imaginary" types are known to it but remote to it. |
This comment has been minimized.
This comment has been minimized.
|
@arielb1 Right, that's what I thought it meant :) It's a nice idea to ensure that this is sound; I don't expect this will occur often enough to be a problem wrt. confusion. |
This comment has been minimized.
This comment has been minimized.
epage
commented
Sep 12, 2018
|
Would we ever consider changing the default visibility in a new Edition to bring the default visibility for trait impl's in line with structs, struct members, and functions? |
This comment has been minimized.
This comment has been minimized.
|
That seems like a pretty big change, even for an edition. |
burdges
referenced this pull request
Oct 3, 2018
Open
Put HashMap's hasher in an Result and Default::default() it to Err(Default::default) #2551
This comment has been minimized.
This comment has been minimized.
burdges
commented
Oct 3, 2018
|
I'd expect the push for genericity will create more situations where crate authors might hide As an example, if you create a There are also plausible concerns around |
Centril
added
A-traits
A-typesystem
A-impls
A-privacy
A-syntax
labels
Nov 22, 2018
This comment has been minimized.
This comment has been minimized.
graydon
commented
Jan 12, 2019
|
Opposed. Case it addresses is not common enough to warrant extension, cost is nontrivial, risk of mistaking it for modular instances is high, and existing pattern of using private newtypes is sufficient and not too burdensome in the rare cases this occurs. |
This comment has been minimized.
This comment has been minimized.
epage
commented
Jan 12, 2019
imo One of the use cases is very common but possibly regularly overlooked in crate API design (and buried in the RFC among talk of coherence / newtype). From the RFC
The case I'm concerned about I think is the same that happened for regex. When creating an
Personally, I'm not seeing it as something that would be mistaken.
I can't speak to cost or whether this causes any soundness issues. If this is possible, I think it would be a big help to reduce overhead for API design best practices (currently more trivial to implement |
This comment has been minimized.
This comment has been minimized.
Unfortunately, the current state of this proposal does when combined with specialization (see #2529 (comment), #2529 (comment)). For a possible resolution of the soundness issue see: #2529 (comment). Since this is rather subtle and not clear whether this is sufficiently scrutable for users, I have been putting of further work on this RFC for now in favor of more pressing concerns. As for cost, @nikomatsakis would know more; I think Chalk should make this easier. |
This comment has been minimized.
This comment has been minimized.
CAD97
commented
Jan 12, 2019
•
|
Where I very much want something like this is for implementing traits that mention types from private dependencies. If you'll note, all the concrete examples of use of this potential feature fall into this case. If we restrict it to this case, standard orphan rules should cover all potential problems with hiding the implementation, so it wouldn't have to "leak" into error messages. I agree that the "full" scoped implementation is problematic. But I think crate-local implementation of traits mentioning foreign types is a less problematic subset. (EDIT: specialization and blanket implementation make this not perfectly true. I still think that the "private dependency" case should always be sound, though, because the implementation can always be added later, right? Specialization is a hard problem, and I haven't really kept up with that nor its relevance to this.) Are you in opposition to just that subset, @graydon? It could be accessible via a broader public/private dependency split (which would otherwise have to forbid said implementations entirely) or more granularly (to allow use with public dependency types). At the very least, I don't think |
This comment has been minimized.
This comment has been minimized.
graydon
commented
Jan 12, 2019
|
@CAD97 not sure, that's a different proposal than what's on the table here. I like minimizing accidental exports and global leakage (indeed it's why I argued against having typeclasses initially) but the ship sailed: reasoning about the impl relation is global. Starting to invent secondary ways of making it non-global in addition to the visibility rules around the traits and types involved in the relation seems to me like inviting more confusion than you could gain from avoid from the cases the existing system is misused. |
Centril commentedAug 24, 2018
•
edited
Allow a visibility modifier on a
traitimplementation such that:An example:
To @aturon, @Mark-Simulacrum, and @kennytm for reviewing the draft version of this RFC.