Sealed Traits

Motivation

There is a large gap between traits and enums. Traits give you lots of freedom when developing an API, and make it really easy to extend the API to handle types that weren’t handled before, or let users implement it for their own types. Traits can also be dynamically dispatched which is allows for code to handle a variety of types that it may not even know about! But this comes at a cost, it uses virtual dispatch, which is slow in comparison to generics or enums.

Enums on the other hand allow you to make a set of types which is internally maintained, but can often be hard to extend, due to stability concerns, and introduce an overhead that often isn’t necessary in the form of a discriminant or wasted space in enums with a large disparity between variants. Enums are also very fast, and this is very nice.

But there is no middle ground, a place where you can get the speed of enums, but the ease and efficiency of traits. This is the space that SealedTraits fill in.

Unresolved Questions

I don’t like that you have to specify the enum every time you need to use a variant (too much of an ergonomic tax for something like this). I find this to be more practical, because I can specify the types in different modules in the same crate allowing me to be more organized. Also, I find this to be easier to teach and understand, at first you could just say that it’s just traits that can’t be implemented outside the module and the speed benefits it provides, then you can teach the benefits with respect to static vs dynamic dispatch (which can be more complicated). Also this proposal allows you to use the types without needing to have a discriminant.

Because the sealed-quality of a trait is so intrinsic to its identity, I think having actual syntax for it rather than an attribute is ideal. Personally, I like spelling this as enum trait. This is because this basically is enum variants are types, just a) requiring you to actually spell out the types (thus you can use privacy in them, yay!) and b) allowing types to belong to multiple enum trait. It also avoids a new contextual keyword.

To be more specific, this would be closest to “preexisting types as variants”.

An alternative within the same design space is to make dyn EnumTrait be !Sized, and put the enum discriminant in the data part of the fat pointer. This loses the key benefit I see in the proposal – being able to use dyn EnumTrait as a error type.

We could add dyn Enum as a thing, for the few cases where that might be relevant. It won’t be usable where a variant is expected, but something like Enum::<Enum::<Enum::Variant>::Variant>::Variant could become an dyn Enum.

We could add dyn Enum as a thing, for the few cases where that might be relevant. It won’t be usable where a variant is expected, but something like Enum::<Enum::<Enum::Variant>::Variant>::Variant could become an dyn Enum .

It’s not as rare as you think, any Tree structure requires this, and that can be common, one example is the DOM.

But yes, this shows that if we allow freely parameterizing implementing types of an enum trait, it prevents dyn enum trait: Sized, as you can’t have enough information; there are an infinite number of variants. In fact, the whole idea of representing dyn enum trait as the type + an enum discriminant breaks down; any generic parameters will result in necessitating a vtable dispatch rather than a simple discriminant.

If sealed traits are spelled #[sealed] trait, it’s definitely a very hard sell to put any kind of restriction on the trait other than that it can only be implemented within the same crate.

If it’s spelled enum trait, and the goal is to be somewhat of a middle ground between enum and trait (which I think is a good idea that we should flush out a bit more), I could see this restriction being argued for. To formulate it: "every generic parameter used to impl an enum trait must be used to parameterize said enum trait"; that is, if you do impl<T> EnumTrait for _, T must be provided as a generic parameter to EnumTrait, otherwise it’s an error.

And again, if said restriction doesn’t exist, there’s nothing making a sealed trait special other than being unimplementable outside of the crate (though that still could potentially be used for optimization).

Now, let’s examine the relation both to “variants are types” and “types as variants”.

Any of these three probably could be made to work, but each has their drawbacks.

The primary drawback to “variants are types” is that Maybe::Justisn’t a type. You need to specify the type on Maybe rather than Just. (Though, I suppose, it would be possible to not do it that way, though this seems the most consistent? Maybe? In any case, that’s path to the variant today.)

The primary drawback to “types as variants” I see is duplication between the external type and making it a variant; additionally, the way matching to destructure would work seems unclear.

The primary drawback to “enum trait” is what I mentioned above; applying enum restrictions on internal “types” to the implementations of the enum trait. Also, the method of doing downcasting is as of yet unspecified, but would probably work like a small Any over the small discriminant.

Having enum trait: Sized is what makes this concept so appealing to me. This halfway point between enums and traits seems to offer much of the benefits of both while only really applying the limitations of enum.

Why would this be disallowed? Also what are enum traits? I have never heard of them.

CAD97:

If sealed traits are spelled #[sealed] trait , it’s definitely a very hard sell to put any kind of restriction on the trait other than that it can only be implemented within the same crate.

Why? What other restrictions are there? The only other thing I specified is the relaxing of the orphan rules and the representation, which could both be done with an attribute that the compiler recognizes.

Just another way of spelling sealed trait. I prefer spelling it as enum trait since the properties we’re going for are a mixture of enum and trait.

Yato:

Why? What other restrictions are there?

I was talking in terms of the restriction that would be required to get dyn EnumTrait: Sized. And in fact, what I’m arguing is that without said restriction (putting generic implementation that doesn’t represent in the enum type), you can’t have a simple discriminant and instead need a full vtable to suppot it, so you just have the implementation restriction, and you don’t have the representation guarantee (though it could apply as an optimization when that isn’t the case, but it could for regular traits as well in an application (not a dylib) anyway, as all implementors are known when compiling a binary).

Yato:

This can be done today (in a way)

It definitely can, but it’s not stopped it from being brought up before! That section was mostly to compare the different ways of formulating a similar construct as the one that we’re discussing, being seal trait/enum trait depending on how you spell it.

I was talking in terms of the restriction that would be required to get dyn EnumTrait: Sized . And in fact, what I’m arguing is that without said restriction (putting generic implementation that doesn’t represent in the enum type), you can’t have a simple discriminant and instead need a full vtable to suppot it, so you just have the implementation restriction, and you don’t have the representation guarantee (though it could apply as an optimization when that isn’t the case, but it could for regular trait s as well in an application (not a dylib) anyway, as all implementors are known when compiling a binary).

The restriction would require negative trait bounds, because it would need to disallow any type which implements the trait. (badly worded)

This would have to be disallowed, or the constraint that L != Cons<_, _> would have to exist, but this constraint doesn’t exist in Rust.

Why? because without that constraint I could have Cons<_, Cons<_, Cons<_, ...>>> (arbitrarily deep), and it wouldn’t be possible to figure out the size of dyn List<_> at compile time for any non-zero sized type.

You could say that a weaker constraint would work, like L: !List<_>, but we don’t have negative trait reasoning either, and that would defeat the purpose of this List<_>. I don’t think it is worth pursuing dyn SealedTrait: Sized, because generics completely break this.

"every generic parameter used to impl an enum trait must be used to parameterize said enum trait "; that is, if you do impl<T> EnumTrait for _ , T must be provided as a generic parameter to EnumTrait , otherwise it’s an error.

The fact is that with anyL provided, you cannot know the size of dyn List<T> if Cons<T, L>: List<T>, because you don’t know L, which could be of any size. And because of this, you need a vtable to handle any interaction with the dyn trait object, so you lose any benefit beyond that of merely preventing implementors outside the crate. That is,

Yato:

the trait object of sealed traits will be a fat pointer, where the extra data is a discriminant which specifies which type is being used.

due to this, SealedTrait can be dynamically dispatched in the same way as enums making it as fast as enums with all the flexibility of traits

cannot be true if you allow any generics beyond those bound by the seal trait itself.

In other words, what’s the benefit of sealed traits, beyond being unimplementable (which is the point of this take 2 as I understand it), when you allow generics on implementors that aren’t bound by the trait.

In other words, what’s the benefit of sealed traits, beyond being unimplementable (which is the point of this take 2 as I understand it), when you allow generics on implementors that aren’t bound by the trait.

I am starting to wonder this myself. I don’t think there is any great benefit to this take 2, some minor changes to the original RFC would be enough, just relaxing orphan rules and adding the details about how sem-ver changes.