Pre-RFC: Sealed traits


#22

Can you do this now?

pub(crate) trait Sealed { }

#23

With https://github.com/rust-lang/rfcs/pull/2028 something like this will be available as well:

pub trait MyThing {
    fn public_method(&self);

    pub(self) fn private_method_that_cannot_be_implemented_outside_of_this_module_making_the_trait_sealed();
}

#24

Unfortunately, no: https://is.gd/wfpcIp


#25

Isn’t unsafe trait (also) an appropriate solution to this?


#26

Despite the fact that it seems like a hack or bug and not an actual language feature I guess I would choose this if I absolutely don’t want anyone implementing my trait and/or I have private methods/objects I also don’t want to expose (defined/implemented on Sealed).

The way I understand unsafe trait is that it can still be implemented by API users, but at least it requires an unsafe opt in. Would also potentially expose methods/objects that I might only want to be private. So it’s not an exact drop-in replacement for #[sealed]


#27

Using unsafe makes it more like a social contract than a compiler-enforced one.


#28

Using unsafe as a makeshift sealed traits feature also dilutes the “here be Segmentation Fault” meaning of unsafe by provoking creation of perfeclty safe (with respect of memory safety) crates that do nonetheless contain The Dreaded Unutterable unsafe (and can’t work under #[forbid(unsafe_code)] and don’t get hypothetical “I’m unsafe-free” crates.io badge ).


#29

(I was responding specifically to @TheDan64’s use case. The whole point there is that downstream crates shouldn’t declare any unsafe impls because it would be memory-unsafe. I appreciate that sealed traits would be even more airtight, but the specific problems you cite don’t really apply.)


#30

Something for which compiler support for sealed traits would be highly useful would be non-public methods in sealed traits.

For instance, I currently want to have a marker trait used in my library that’s used to restrict generic functions to some traits from my library. But these generic impls must now depend only on the interface of the marker trait, which means it has to export enough functions to be usable from said generic functions. And so these functions become part of the public API.

In code, here is what I’d like:

#[sealed]
trait Trait {
    pub(crate) fn foo(&self);
}

struct Struct {}
impl Trait for Struct { fn foo(&self) {} }

fn bar<T: Trait>(t: &T) { t.foo() }

Does what I’m hoping for make sense? Currently the best way to do it appears to be #[doc(hidden)]… or defining a private struct in my library and taking it as an argument.

I don’t see any way other than sealed traits to do this cleanly, as pub(crate) would make no sense for non-sealed traits.


#31

@Ekleog have no hard opinions on #[sealed], but as I understand it trait methods are currently always public no matter what, so the pub(crate) for method foo would require a further language change at the very least.


#32

I think another thing to note is that sealed traits allow for a more gradual shift from enums to traits and vice versa.

Extendable from outside Can be used as Constraints
enum No No
sealed traits No Yes
traits Yes Yes

It seems like the perfect fit for something between enums and traits.


On this note, trait objects of sealed traits don’t need to store a vtable and perform dynamic dispatch, they could just store an index which specifies which type they are and use that to figure out which impl to call, thus making them effectively as fast as enums, while still being a trait!


~edit~

Also on the note of using indices to differentiate types, this could be used to provide a way to downcast references into their concrete types for all sealed traits.


#33

FWIW, Kotlin sealed classes are basically this approach to sealed traits, though also being the “data carrying enumerated type” and using the JVMs casting support still.

I wonder if a proc macro could be written to implement this in user code… it would also basically be equivalent to “enum variants are types” while the restriction of implementors being in the same file (macro block) holds.

(I do not have time and should not implement this but)

sketch
#[enum_trait]
pub trait Example {
    Case1(String),
    Case2 {
        alpha: usize,
        beta: isize,
    }
}

// translates to

mod $unnamable {
    pub trait Sealed {}
}

pub trait Example: $unnamable::Sealed {}
pub enum dynExample {
    Case1(Case1),
    Case2(Case2),
}

pub struct Case1(pub String);
pub struct Case2 {
    pub alpha: usize,
    pub beta: isize,
}

impl Example for Case1 {}
impl Example for Case2 {}

// plus handle conversions via (Try)Into
// and other derives as well

But having written that, I like the idea of having sealed traits’ vtable instead be an index, so dyn SealedTrait is just the regular struct and the fat pointer has the enum discriminant.

The comparison to “enum variants as types” stands, though.