Pub trait in private module: no "can't leak private trait" error

Hi,
I hit this restriction

pub fn generic<T: Private>(t: T) {} // error: can't leak private trait
trait Private {}

and while playing with the pattern of sealing a trait I found that the following works

pub fn generic<T: private::Private>(t: T) {}
mod private {
    pub trait Private {}
}

I'm wondering why the two cases above are treated differently. The two seem equivalent to me, maybe I'm missing something? I took a look at the Rust book and reference and they don't seem to address this, rustc --explain E0445 explains how to do the workaround but not why it's allowed while the former example isn't. Thanks

I don’t know if it’s part of the intentional design, but the latter pattern is often used to “seal” traits, preventing downstream users from implementing them. This is usually not a good idea, but is sometimes desirable when you’re doing some more advanced type system tricks, like using it for real computation.

In particular: if your crate relies on knowing the full set of types that implement a trait, this is a way to prevent external crates from breaking yours.

I agree that theoretically they could be considered equivalent, but the difference in treatment allows the following trick.

In the second case, you could do something like this, which compiles:

mod lib {
    mod private {
        pub trait Private {}
    }
    
    pub fn generic<T: private::Private>(t: T) {}
    
    pub struct Struct;
    impl private::Private for Struct {}
    
}

fn main() {
    let s = lib::Struct;
    lib::generic(s);
}

From lib you can export public types which implement the private trait. From the outside you cannot create new implementations of Private, of course, that's why it is considered "sealed".

I believe originally, both cases should have been disallowed. But the latter case was unfortunately allowed for many compiler versions, and the sealed trait pattern @2e71828 mentioned was developed using it. The "bug" has been here since rust 1.0 (though I'm not sure when the first usage of the sealed trait pattern occurred).

There have been efforts to get the latter case fixed, but they've all been pushed back because of how useful the sealed trait pattern is, and the fact that we really don't have any other way to do that. For instance, see posts like https://internals.rust-lang.org/t/the-future-of-privacy-and-encapsulation/2961/7 (Dec 2015), https://internals.rust-lang.org/t/pre-rfc-sealed-traits/3108/2 (Jan 2016), https://internals.rust-lang.org/t/pre-rfc-warn-about-inaccessible-types-in-public-function-signatures/5156/8 (Apr 2017).

There appears to even be an unimplemented RFC for fixing this - the tracking issue is https://github.com/rust-lang/rust/issues/48054? I'm not 100% sure this is what this is, but it seems like it.

Having existed for many years now, though, the sealed trait pattern itself is well documented. It's even in the API guidelines, see https://rust-lang.github.io/api-guidelines/future-proofing.html#sealed-traits-protect-against-downstream-implementations-c-sealed

4 Likes

Thanks all, particularly for the internals/RFC links. I had seen the pattern previously in the form described by @FedericoStra, it makes more sense to me as it's arguable that you're not leaking a private trait there. But the second example in the OP compiling came as a surprise to me. Hearing that this "feature" came about somewhat accidentally explains a lot. I'm also not sure how exactly the unimplemented RFC interacts with code like this, but it does seem like it's meant to address things like this.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.