Sealed traits

But it could also be pub mut(in self) x: i32 and still be orthogonal. (Or, of course, an attribute.) And if it needs to support pub(in super::super, mut in super) because we need to support both kinds of things -- or more things! -- then the construct just starts to feel cluttered to me.

When pub(mut in self) trait Foo { … } doesn't make sense and struct Bar { pub(impl in self) a: i32 } doesn't make sense, it doesn't feel like they should both be in the same construct.

Not to mention that I'd rather be able to search "rust sealed trait" and get something useful. If I look for "rust pub impl" I'd probably get a bunch of things talking about "no, there's no privacy on impls", not what I want.

4 Likes

An alternative to #[sealed] as proposed in the original post, yes.

Granted. The syntax is bikesheddable, but something along the lines of this? I view the value of more fine-grained control and future ability for read-only fields quite highly.

1 Like

I agree, it sounds like a nice features to have, since seeing the field -- even if non-mutably -- can help with borrow splitting and such, unlike a method for it.

2 Likes

I would really, really like to have method visibility implemented together with this. The main reason is that things are normally private by default. There's no priv keyword, just pub. If all methods are public then adding visibility support later would require some special notation, making it confusing. As an alternative if all methods are private by default people can add a second non-sealed trait but that brings us back to the current situation.

Further, it's not possible to have truly private methods and bounds:

pub trait Actual: sealed::Sealed {}

mod sealed {
    pub trait Sealed: Copy {
        fn foo(&self);
    }
}

// impl Actual, Sealed

in consumer crate:

fn do_foo<T: Actual>(val: T) {
    // consumer crate can assume `Copy`
    let val2 = val;
    // consumer crate can call "private" methods
    val.foo()
}

In many cases it's useful to have private implementation details as methods on the trait or even private trait bounds.

I propose that methods and trait bound of sealed traits are invisible in dependencies except were marked pub

pub(impl in self) trait Sealed: pub Copy + Debug, /*not visible*/ Self: Display {
    // can be called from consumer crate
    pub fn foo(&self);
    // can *not* be caleld from consumer crate
    fn bar(&self);
}

That is a fundamentally different problem, and one that I do not intend to solve alongside sealed traits. At some point I do intend to write an RFC for proper visibility on trait items. But there is zero reason to do them together.

And by the way, we do have a priv keyword.

That said, if sealed traits are e.g. pub impl(in crate) rather than #[sealed] pub, having member visibility limited to the impl visibility by default should at least be strongly considered. I'm fine with #[sealed] just controlling the ability to impl outside the coherence boundary, but anything getting more first-class syntax has a stronger implication.

It is also an interesting observation that just pub impl(in crate) is (nearly[1]) sufficient to express member visibility as well. In fact, the most natural way to express impl visibility is via member visibility: if you can't name a required member, you can't implement the trait. Voldemorting a supertrait is currently the most common way to seal a trait, but it's also possible[2] and not uncommon to seal a trait via a #[doc(hidden)] required method taking a pub-in-priv Voldemort type as an argument.

This also just imho matches Rust's priv-by-default stance better. Members are public to the scope that can implement the trait by necessity, since you can't implement the trait without visibility of the members (as above). If this necessity no longer exists, the members should take on the default state of private to the implementing scope.

I agree in restricting the scope of #[sealed] to standardizing : Sealed behavior and providing better compiler error support by understanding the pattern more directly. But actual "impl visibility" syntax is a different beast with different implications.


  1. Fully sufficient if you're willing to introduce extra traits to capture the visibility splits; it's possible to construct use cases which want the higher control given by per-member visibility. ↩︎

  2. This is less common and I'd argue inadvisable, since it's possible that a way to implement a function without naming the type could be introduced in the future, either by accepting trait method argument type as a defining use position for TAIT, or some other form of allowing type inference of "use what it says in the trait." This also fits well imho with overconstraining impls; there's no inherent reason why I shouldn't be able to implement a trait function more generally by e.g. taking impl Sized, other than this allowing me to define a method being used to seal the trait via Voldemort argument. ↩︎

1 Like

As in, have the impl restriction imply non-public trait items? That seems quite subtle. I want non-public trait items too, but to me the most reasonable way forward is to do it in an edition (changing the default to private).

This is the same situation that led to #[non_exhaustive] — people don't like doing that. It requires an additional method for no purpose other than restricting the ability to implement the trait. It also makes it easy to accidentally unseal a trait if you make the last private method public without adding another private method.

My observation here is that impl-visibility requires member-visibility. So not quite

but rather that trait members are always non-public, but that providing impl access also implies providing pub access to the members.

I.e. like not writing pub on items defaults to pub(in mod), not writing impl defaults to impl(in extern), and not writing pub on members defaults to pub(in impl) (and that doesn't mean protected, that means public to modules with impl visibility).

The overwhelming majority case for traits is to have the members visible to the world for implementing, and not just because that's the only currently supported option. #[doc(hidden)] is quite functional as a for-internal-use-only marker. std has stability. Everyone has access to pub-in-priv sealing. And yet most traits do want the pub-in-impl-scope behavior.

It becomes less problematically subtle if you reframe from a restriction to a visibility scope. It's just that pub trait means pub(in extern, impl in extern) because that's by far the most common configuration.

Thus why I'm not saying to implement sealing that way, just that an unnamable required member is the simplest way to model a trait being sealed.

(Actually, can #[sealed] be spelled #[non_exhaustive]?)

2 Likes

I completed a speculative implementation of pub impl(crate) trait Foo {} syntax. It's on my fork of rust-lang/rust if you'd like to check it out. Trait item visibility is definitely not required. Keep in mind it's the trait definition that has the restriction, not the trait implementation or trait items.

This was proposed and discussed earlier in this thread.

(Don't have the bandwidth to play with a custom rustc fork atm)

Are you allowing

mod a {
    pub impl(crate) trait Spam {
        pub(mod) fn spam();
    }
}
mod b {
    struct S;
    impl crate::Spam for S {
        fn spam(); // allowed
    }
    fn test() {
        S::spam(); //~ ERROR: not visible
    }
}

If so, this is specifically what I'm arguing against. Implementing a trait requires[1] visibility of all non-defaulted trait items.

As such, my argued position is that impl(in $path) is a suitable and potentially desirable way to opt the default member visibility down from pub(extern) to pub(in $path). And from that rule, an implicit impl(extern) means the default implicit member visibility is pub(extern).

Separating "impl visibility" from "use visibility" for traits makes sense. Separating the two concepts for trait items, especially where the "impl visibility" scope can be larger than the "use visibility" scope, seems highly undesirable.


It's worth reiterating that any restrictions apply solely to the annotated item; e.g. pub(self) mod x doesn't prevent doing pub use x::PublicItem.

Under this interpretation, it makes sense that pub(in path) impl(in path) trait has no impact on the visibility of the trait items; they're all pub(extern), and the restriction comes from restrictions on the path-to the item.

The difference is that every item which isn't a trait associated item defaults to pub(mod) if pub is not specified. So my concrete proposal is to define the reason for this as being that trait associated items have a default unspecified visibility of the trait they're on's impl visibility.

You can still change the visibility of a trait associated item by giving it an explicit pub annotation. It can still be arbitrarily greater or lesser than the trait's impl visibility, but this provides a reason for the difference to the priv-by-default convention (visibility of the (non-defaulted) members is required in order to implement the trait) and still offers full control.


Interesting aside: would it make sense to allow pub impl(mod) struct S? The semantics would be disallowing impl S outside of the module; treating S as foreign w.r.t. coherence. This would be an interesting way of bringing module and crate barriers closer together in expressive power.


(:salt:: I wish we could've sync discussed this at RustConf. Stupid planes.)


  1. Caveat: default allows partial trait impls which make things all sort of complicated. ↩︎

Sure, just saying it's there :slight_smile:

pub(mod) fn spam() is itself invalid — it would remain fn spam(). Visibility is not added to trait items. The only syntax addition is pub trait Foopub impl(crate) trait Foo. No other syntax is added in my (in-progress) proposal. The only thing introduced here, as you put it, is "impl visibility", and that's for the trait as a whole. "use visibility" and/or "impl visibility" for individual methods is something I want, but not as part of this proposal.

TL;DR for what I want is "you can see and use the trait, but you can't implement it".

Correct. This will not alter priv-in-pub in any manner.

Conceivably. I think the use case is less here, so I'm not including it alongside impl-restricted traits and mut-restricted fields.

:100:

and stupid AA — that's where the issue was truly at

I have encountered a use case which, I believe, cannot be solved by the usual sealed trait pattern and macros (i.e. a private trait bound).

There is a trait Foo defined in crate A. In my crate B, I want to add an extension method to all instances of Foo, which does a specific thing based on the methods provided by Foo. But Rust doesn't allow extension methods, only extension traits.

For this reason I introduce trait Bar: Foo and a blanket impl

impl<T: Foo> Bar for T {
    fn bar(&mut self, v: V) -> &mut Self { ... }
}

Now, currently it means that Bar cannot be implemented outside of B: any implementor would have to impl Foo, and then would be covered by the blanket impl. But if specialization ever lands, then it will likely become possible to add a specialized impl of Bar for the foreign type.

This would break my assumptions, since I rely on a specific behaviour of bar. Really, bar should have been a generic function

pub fn bar<F: Foo>(foo: &mut F, v: V) -> &mut F { .. }

But then, if I need to call it several times in succession, instead of a clear chain

foo.bar(v1).bar(v2).bar(v3)

I would have complex nesting

bar(bar(bar(foo, v1), v2), v3)

I don't think there is currently a way to restrict implementations of the trait while keeping the set of implementing traits open, which wouldn't be broken by specialization.

2 Likes

@afetisov This is a good example! I think we should capture this example in the RFC for sealed traits, insofar as it's motivation for "this can't just be done with the common sealed-trait pattern". (That's in addition to the motivation of wanting something simpler and clearer than the sealed-trait pattern.)

1 Like

Doesn't specialization require an opt-in (default) before more specific impls are allowed? Then you would just not do that for Bar.

5 Likes

You could have a situation where you needed to specialize internally but didn't want to allow specializations in external crates though, couldn't you?

We already do that in the standard library by forwarding to an internal trait, e.g. SpecExtend. This way the public API does not expose the ability to specialize.

ToString is an exception that does have public specialization, because this crosses crate boundaries from alloc to proc_macro. Sealed traits would not help avoid that case, but I suppose we could add an unstable pub trait SpecToString that proc_macro is allowed to use.

3 Likes

I think this is something else that came up in zulip conversations as a related-but-different feature: #[final] methods in traits.

Basically, the problem is less that you don't want anyone else implementing the trait, but that you don't want anyone else changing the implementation of the method. And that's a separable problem -- for example, it might be nice to have

trait Copy: Clone {
    #[final]
    fn copy(&self) -> Self { *self }
}

where we obviously need Copy to be un-sealed, but it would be nice to let unsafe code rely on the method not doing something different.

That would also be a principled way to put methods on marker traits (marker_trait_attr - The Rust Unstable Book), and would be a nice feature in various other places too, like the super_* methods rustc has on visitors that should never be overridden.

9 Likes

I have found a new approch for this proposal

1.creating a new builtin trait for finding out if the type is local or foreign

let's say the new trait is called type_address.The implementation is

'''rust

trait type_address{

type position;

}

''' every time a new crate let's say crateA is created the compiler will internally implement a new unit sturct type,let's say DemoStruct.it will not be inside any builtin library crate,the unit struct belongs to that crate

Now in the crate the compiler will automatically implement type_address<position=Demo_Struct> trait for every new type created in the crate.

After that the compiler will check for orphan rule by looking the trait.

the compiler will check if a type is local to the crate or not,by checking if it implement the type_address<position=Demo_Struct> trait,now as every crate create the Demo_Struct type,so crateA::DemoStruct!=crateB::DemoStruct, every crate will implement unique type_address So no trait overlapping would be possible

##things you could do##

1.create sealed trait

trait sealed_trait:type_address<poistion=Demo_struct>{}

2.implement generic

trait can_debug:type_address<poistion=Demo_struct>{}

impl<T:can_debug> Debug for T{}

Every behiavour a type posses is determined by it's trait.So is the type local to the crate or not should also be determined by traits.Thats what I think.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.