-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Ban private items in public APIs #136
Ban private items in public APIs #136
Conversation
FWIW, this is actually partially implemented in the form of the struct Private;
pub type Public = Private; trait Private {}
pub fn public<T: Private>() {} Both of these were explicit decisions, since they essentially allow exposing a public interface around private types. The trait one I regard as particularly important, since it is the only way (that I know) of exposing a polymorphic interface while disallowing it to be overridden (or methods called) by others, e.g. the trait bound may be used by function doing ridiculously I'm in favour of extending the lint's rules to include Also, this is closely related to rust-lang/rust#10573. |
Yeah I know, probably should've mentioned it.
I know about this trick, but is it actually used anywhere? It feels kind of hacky to me, and I would rather have some kind of formal mechanism to accomplish this (like maybe
Ah yes, thanks for unearthing the evidence for my point in the Motivation :)
|
Multiple people have asked about this on Reddit. I've used it in my library. In my case, I didn't care about the external implementations, rather I wanted to implement default methods of a public trait in terms of the methods provided by the private super-trait. |
I added some of the mentioned examples to the Motivation and Drawbacks sections. |
I don't think the drawbacks section accurately portrays the practical use of the private super-trait idiom. In my case, the pattern like looks like this: struct PrivateData
{
data: int,
}
trait PrivateTrait
{
fn get_data<'l>(&'l self) -> PrivateData;
}
pub trait PublicTrait : PrivateTrait
{
fn public_function(&self) -> int
{
self.get_data().data
}
} Making everything public as required by this RFC would leak those internal implementation details. In principle I could mark those API entries as permanently unstable, but surely that's what the privacy system should be used for. |
The D language has this - under the name Voldemort types - and considers them a feature rather than a drawback. |
@SiegeLord I'm fairly certain your use case could be satisfactorally reformulated using In any case, you've offered a quasi-objection but not a proposed resolution. What would be your preference? To do nothing? To carve out a special exception for private supertraits? Would it be intolerable if this functionality was lost even temporarily? Would adopting this rule only be acceptable to you if the @AbigailBuccaneer Our (better designed, more principled) equivalent to that functionality would be RFC PR #105. From a philosophical perspective (which is important!), semantic invariants should be upheld using semantic means whenever possible. (To put it another way, in the typechecking and other semantic analysis phases, rather than in the name resolution phase.) If the safety of your abstraction rests on the fact that external code can't write the name of a thing, that's brittle. C++ libraries had a technique of relying on private classes returned by public functions to prevent them being used except as temporary rvalues, because their name couldn't be written, which suddenly broke when C++11 added In this particular case, it's admittedly unlikely that we would gain something like EDIT: @SiegeLord: Just to clarify, forbidding private supertraits is not a hill I would choose to die on, and I'd be mostly fine with carving out an exception. If we can outlaw the other 95% of cases, I'll be 95% satisfied. And the process could just as well be structured to first outlaw the other 95% in the first phase, and then remove private supertraits together with introducing the replacement functionality in the second phase (as long as that happens before 1.0, of course). I just wanted to make clear why I think consistency is the better option on the merits, and to try to suss out what you think. |
I think an exception to private super-traits it something that I would be content with. To me, private super-traits of public traits are closely related to private fields in public types. I.e. having a private field shows up in the documentation (in the sense that its presence is indicated by a comment) and it prevents you from instantiating an instance of the type manually. Private super-traits seem to act the same way except in type space: they (could) show up in documentation as a comment, and they prevent you from 'instantiating' a new trait/type that implements it. I've never studied type theory, so I don't know if this analogy is valid. Either way, to me, private super-traits are not members of the public API any more than private fields are. In this sense, banning all the other cases of private items while allowing these is perfectly consistent, at least in my view. In particular, a private super trait does not have to show up in function signatures etc to be useful, e.g.: trait A
{
fn private_method(&self) {}
}
pub trait B : A
{
}
pub fn public_function<T: B>(a: T)
{
a.private_method();
} |
I'd still be curious about further details of your use case mentioned in the previous comment. |
There is no |
Servo also uses this. |
As noted above, we could simply carve out an exception for private supertraits for now, which seems to be the only controversial part of the proposal, to keep that kind of code working. We could then revisit it later with potentially a more proper solution if we want to. |
I wouldn't really want this to be removed until we have actual abstract types in modules. I'm fine with it being feature-gated forever, though, and that might actually be the right thing to do, since we wouldn't want to support this if we did have abstract types. Since Rust has recursive modules, adding abstract types might be nontrivial. |
Which "this"? The whole enchilada? Just private supertraits? Some other specific part of it? (If it's not just private supertraits you're referring to, could you provide an example of what you would use the functionality for which wouldn't otherwise be possible?)
Abstract types in what sense? To put it another way, what difference are you thinking of relative to structs with private fields?
I'd be OK with feature gating it, but I also don't see why it's desirable to keep it. From my perspective it's just tying up loose ends and removing the possibility of weirdness. |
This seems like a mistake. I would think there's a definite use for being able to use a // private struct
struct MyStruct<T> { ... }
// private marker types
struct MarkerOne;
struct MarkerTwo;
impl MyStruct<MarkerOne> { ... }
impl MyStruct<MarkerTwo> { ... }
// publicize those two variants
pub type StructOne = MyStruct<MarkerOne>;
pub type StructTwo = MyStruct<MarkerTwo>; This would allow me to vend the two types I think the better solution is to merely consider that a |
Currently I think the primary problem here is that private types can leak in public signatures and bounds - and would prefer to prevent only that. |
I'd love to see this implemented, but on the other hand, there really has to be a way to share private members between modules. In implementing a wrapper for a somewhat large C API, I tried to chuck it up into modules (https://github.com/tupshin/rust-cassandra-wrapper/tree/master/src/cassandra). Unfortunately, there is no adequate way right now to have structs wrapping unsafe private structs, and still adequately modularize the implementation. I don't know if there's a better way, but "protected" visibility, so it can be seen by sibling modules, is what comes to mind. |
@tupshin I take it that you were unable to structure your code in a manner like this? Example playpen code illustrating sharing a private type among child modules. (Note that this works even when you break the nested submodules |
Would this prohibit this pattern, which was suggested to me and I find quite useful?
Rationale and motivation for this (as well as a link to sample code) are in this thread: https://mail.mozilla.org/pipermail/rust-dev/2014-January/008033.html |
The pattern of having a private field in a struct? No. |
@pnkfelix I did an experimental rework to use that approach in a simple way by moving my structs into mod.rs. https://github.com/tupshin/rust-cassandra-wrapper/blob/master/src/cassandra/mod.rs#L28
|
I feel like this RFC is a good idea, but I've been trying to think of a crisper pitch than "allowing this feels weird, and we should be conservative." One concrete problem with allowing private items to leak is that you lose some local reasoning. You might expect that if an item is marked private, you can refactor at will without breaking clients. But with leakage, you can't make this determination based on the item alone: you have to look at the entire API to spot leakages (or, I guess, have the lint do so for you). Perhaps not a huge deal in practice, but worrying nonetheless. Put another way, it seems useful to have privacy as a strict and simple way of knowing what downstream clients can depend on, and what they can't. Allowing private supertraits seems OK because, assuming all the other leakage checks are in place, clients have no way to depend on or use the supertrait. It would effectively be a marker that the child trait is unimplementable by the client, and could be shown as such in rustdoc. It also seems like the use of Assuming these particular cases can be cleanly handled, what's the argument for not doing this? |
That's the wrong way to think about it. We don't put everything in the language and then argue to remove things. We keep the language as simple as possible and argue what to add. Your points about local reasoning seem like they should be satisfied by the Given that we already have a lint to flag this sort of thing, what do we gain by turning it into a language rule? |
This doesn't seem particularly helpful. For one, this RFC can as easily be viewed as taking away a feature as adding one. But I think that's not really relevant: the real question is about the meaning of privacy in Rust. I was surprised that leakage of private items is allowed, and I'm trying to understand the rationale of the current design, assuming it was intentional. If you do want to make this kind of general point, I think @glaebhoerl's argument in the RFC is the stronger one: absent a clear reason and coherent design for exposing private items, the conservative thing to do is disallow them.
Not exactly. Since lints can be turned off, safely refactoring private items an existing code base requires auditing places where the lint is disabled. If leakage was disallowed by the language, though, you could reason about privacy truly locally. |
Are you concerned about your own code, or are you trying to force what you consider to be good coding practices upon other people? Because if you're concerned about your own code, don't turn off the lint. Personally, I think it's not a language's place to try to force what the author considers good design upon the users. It should encourage good design, but it should also be able to get out of the way and let people do what they really want to, if they really want. This is basically the same reason why I think the RFC about enforcing capitalization rules was a bad idea. We've already shown that there are cases where exposing private types allows you to do things you couldn't otherwise (such as have a public trait nobody outside your crate can implement). The problem with defaulting to being conservative and adding exceptions for things like private supertraits is you can only add exceptions for the things you can think of doing right now. Sticking with a warning instead of a hard rule lets people disable it if they come up with valid reasons to violate the rule. As an example of a valid reason that hasn't been suggested yet, not too long ago (I think in another PR but I forget) I wrote some code for someone that used I believe the only alternative, if this RFC were to be implemented, would be to have to create a brand new type that implemented For what it's worth, my earlier suggestion that a |
This seems like a good way to have our cake and eat it too, in a disciplined manner. . . and I think there is precedent for it in ML, no? (It is possible I misremember.) @aturon in response to your point about using a newtype struct instead of this, do note @huonw 's point that:
I often overlook the latter cost myself. @glaebhoerl do you have any thoughts on this detail (on making an exception to the blanket rule for |
I suppose a problem with making an exception for e.g. as has been said above, what one really wants here is an existential type (or maybe "abstract type", though I guess the two are the same under some mental models), where all you can do is pass around the instance but do not know what its internal structure is. But currently that is not what happens when you mix |
@pnkfelix I gave up trying to relate Rust's decisions to ML modules earlier in the discussion, but ML solves this by separating modules from their signatures and providing a rich signature language. Ascribing a signature to a module may hide type components of the module entirely, make them abstract (in the sense of existential types), leave them manifest. It gets more complex than that (e.g. sharing constraints between distinct modules so that you can control external knowledge of implementation details), but there is a distinction made between namespace visibility and type abstraction. In Rust that distinction is missing, although field privacy can be used to emulate it in some ways. |
@pnkfelix Yes, existential types is what I really want in that example. But as long as we don't have them, I'll settle for a |
What you want is either existential types, "abstract return types" (which are "module-level" existential types), or a newtype.
An issue here is that the semantics of "private items" leaking is unclear (if you impl a private struct with a public trait, can you access it?) - the best way to avoid this kind of mess is to ban these "unprincipled" leaks and support only principled ones. The "allow this - someone may have a use for this you haven't thought of" viewpoint isn't the way the Rust type-system is designed - if the Rust designers haven't thought of a special use for a feature (especially a feature designed as syntactic sugar), it might have weird interactions with other Rust features (see GeneralizedNewtypeDeriving[http://www.haskell.org/pipermail/haskell-cafe/2010-March/074305.html] for such an example in Haskell). This is a distinction between Rust's typesystem and C++ templates, with the latter caring much less about these edge-case (but used-by-clever-developers) interactions. |
As far as that goes, GNTD is very closely related to the |
@glaebhoerl Just to make sure you saw it: this RFC was discussed in yesterday's meeting, with no definite decision yet. |
@aturon Thanks! I saw. And gah, it looks like I've royally screwed up the rebasing. :( What to do? But I've significantly updated the RFC based on the discussion here. |
You should be able to use (Also, theoretically one should never rebase or amend an RFC; just new commits.) |
That seems to have worked. Thanks. (For some reason I had internalized the rule as "don't squash commits", instead of "do not rebase at all".) (And all this time, I had been interpreting |
Hey, privacy experts: Is this actually a problem: rust-lang/rust#17697 ? (I'm trying to figure out if it should be cloned into a fresh issue, as I am doing with others that were closed around the same time by that owner.) |
My first guess is that this is just useless - you can't expose a private type, so there's no way its field being public could have any relevance - and so it's worth warning about, but not imperative to forbid. But it's possible that I'm missing something. |
I have an issue with this RFC: it does not explain what is meant by the word public within the RFC text. What does it mean?
The meaning of this sentence, which is key to interpreting the RFC, is thus unclear:
From the comments at the bottom, I'm guessing "public" means 2. Unfortunately, this still leaves the issue of how to document the "unnameable" type, but I guess that can be dealt with (though it means using the name of a private module, most likely). |
Let's say a function return a value of unnameable type. Then the docs on this function can describe what a user of this function can do with that value. For example, C++ docs do this when return type of something is unspecified. FWIW, I tweaked the privacy pass to apply more strict rules (variant 3 from your list) and run it on rustc and standard library. Most of broken code contained actual deficiencies - types that should have been nameable, but weren't reexported by accident. |
@dhardy |
@petrochenkov I realise the RFC has been modified since this PR, but this is still the place for comments. Also, I am not surprised that variant 3 works for most code. If you read this PR, it reads two uses for exporting "private" (unnameable) items, and in both cases better solutions (type aliases/deriving new-types, and private trait members). IMO that would be the better solution than the current RFC, but it's not really important. What I don't like about current RFC (aside from the contradiction in allowing |
Add contains to includes RFC
Pretty