I don't fully agree with this formulation. If that were the whole story, then we wouldn't bother with any guarantees, right? It'd be perfectly fine to "infer" the type of a local variable to a private type, e.g. in code like this:
// here `returns_some_private_type()` returns a type
// not visible to the current module... we couldn't write
// the type explicitly, but we can *infer* it.
let x = foo.returns_some_private_type();
// Regardless, we are forbidden from accessing private
// methods. We are limited to the public API surface,
// though notably this includes all traits.
x.public_method()
If we adopt the more liberal rules advocated by @petrochenkov (namely, it becomes illegal to have a type that is inferred or normalized to a private type, but it is ok if the type name is not "visible" statically because it can't be inferred or normalized), then the code above becomes illegal, and the guarantees around private types become something more like:
"A private type can only be used from outside the current module via trait methods"
Even here it is not possible, in the strictest sense, to "detect" the type via specialization, because one cannot write the specialized impl since you can't name the type directly. It may be possible to write some indirect impls via associated types, but they will still be limited to trait methods.
If we adopt the slightly stricter version that I proposed (in which we retain a rule prohibiting associated types from being assigned to private types, unless the impl has a private input type), then we get an additional guarantee, which has to do with the origin of the private type. In particular, "original" instances of the private type can only be created within the owning module. They can then escape the module's dynamic scope via returns, threads, or mutation.
So, as a simple example, imagine a private Priv
which implements Default
:
trait Alias {
type Out;
}
mod priv {
#[derive(Default)]
struct Priv;
pub struct Pub;
impl Alias for Pub { type Out = Priv; } // illegal under my proposed rules
}
mod another {
// directly naming the type via this alias is illegal under any proposed rules:
type Priv = <Pub as Alias>::Out;
pub fn foo<T: Alias> {
// but indirectly naming it is ok, and now I can create an instance:
let x: T::Out = T::Out::default();
}
}
fn main() {
another::foo::<priv::Pub>();
}
It seems to me that having control over when values are created is really important, particularly when you have types that represent permissions. That said, probably those types would not implement Default
or any similar trait that lets you synthesize said permissions from "thin air". But I'm not entirely sure about that.
It is also certainly true that the story of said guarantees is decidedly not simple. Your approximation (public API surface is accessible) is a good rule of thumb, though it overstates the truth.
I admit, the analogy between pub use
and an impl didn't occur to me at first. But now that I've seen it I can't unsee it. =)
That said, I think that ultimately we're going back and forth on fairly fine point, and by and large the guarantees you get are very close either way. It comes down to whether we think it's ok that people can synthesize instances of private types "from thin air" outside of the module.