Can you do this now?
pub(crate) trait Sealed { }
Can you do this now?
pub(crate) trait Sealed { }
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();
}
Unfortunately, no: https://is.gd/wfpcIp
Isnāt unsafe trait
(also) an appropriate solution to this?
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]
Using unsafe
makes it more like a social contract than a compiler-enforced one.
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 ).
(I was responding specifically to @TheDan64ās use case. The whole point there is that downstream crates shouldnāt declare any unsafe impl
s 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.)
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.
@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.
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.
~edit 2~
Indexes cannot be used in general due to how amazing generics are.
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)
#[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.
That was exactly what i was thinking of, just couldn't remember the phrase.
I love that this could be the bridge between enums and traits.
Another thing we could guarantee about the sealed traits is that no we canāt, generics donāt allow it.dyn Trait: Sized
where Trait
is a sealed trait and that dyn Trait
is laid out exactly the same as an enum with each variant of the enum hold exactly one of the types. This could be huge as it will enable ergonomic error handling. This doesnāt dilute the meaning of dyn
, it still means dynamically dispatched, it just changes how it is dispatched. Also with this, sealed traits can be object safe even if they have consuming functions (i.e. functions that take self
).
For example (simple example)
trait CustomError { }
struct DivByZero;
struct SqrtOfNegativeNumber;
impl CustomError for DivByZero {}
impl CustomError for SqrtOfNegativeNumber {}
fn fallible(x: f32) -> Result<f32, dyn CustomError> { // note: dyn CustomError would be Sized
if x > 0.0 {
Ok(1.0 / x.sqrt())
} else if x == 0.0 {
Err(DivByZero)
} else {
Err(SqrtOfNegativeNumber)
}
}
And this could just work! It is easily extensible and the users of this code can just use the interface provided by the trait (this is a lot like how Pre-RFC: sum-enums works, but more useful).
I really like this formulation, and it probably deserves an RFC of its own. Given the large semantic impact, it probably should have dedicated syntax instead of just an attributeā¦ enum trait
maybe, which I think captures the benefit of using it.
Itās kind of impressive how many use cases this can serve, especially if it offers a nice way to downcast. While itās not as ad-hoc as anonymous enums or tagged unions, it offers clear subsetability.
I think youāve convinced me that dyn EnumTrait : Sized
is better than sticking the enum discriminant in a fat pointer. Thatās something weād have to weigh pros/cons of in the RFC.
PM me if youād like to collaborate on drafting an RFC. This is definitely something Iād like to see fleshed out further.
I'll start a new thread here on internals, and we can flesh it out there.
Here it is:
Some prior art for sealed traits (āclosed classesā) in the context of Haskell: https://www.microsoft.com/en-us/research/publication/object-oriented-style-overloading-for-haskell/
(I had a comment on one of Nikoās blog posts a few years ago exploring the connections, but it looks like all comments are gone nowā¦)
I think we could do something like dyn SealedTrait: Sized
as an optimization.
We could enable it with something like
#[derive(SizedTraitObject)] // bikeshed
seal trait SealedTrait {}
This would enable some checks
Sized
struct Err<E: Error>(E);
struct DatabaseError;
// Valid
#[derive(SizedTraitObject)]
seal trait CustomError_EX1 {}
impl CustomError_EX1 for Err<std::fmt::Error> {} // all generic parameters are filled, so this is fine
impl CustomError_EX1 for Striing {} // no generic parameters, so this is also fine
#[derive(SizedTraitObject)]
seal trait CustomError_EX2<E> {}
impl<E: Error> CustomError_EX2<E> for Err<E> {} // fine, all generic parameters used on struct are used on CustomError_EX2
impl<E> CustomError_EX2<E> for String {} // fine, String has no generic parameters
// Invalid
impl<E: Error> CustomError_EX1 for Err<E> {} // the type parameter E is not constrained by CustomError_EX1, so this is invalid
impl<T, E: Error> CustomError_EX2<T> for Err<E> {} // the type parameter E is not constrained by CustomError_EX1, so this is invalid
This is similar to how enums currently work.
But at that point you could just use enums, where each variant holds a single type, and has a From
impl for each variant for ergonomics.
I wonder if this āenum trait
ā (dyn Enum : Sized
by treating it as an enum) could work for unsealed traits as well? (Though itās kind of off topic in a thread about sealed traits ) Ofc a dylib makes it break, and introducing features that donāt work in dylib is a big bummer weād like to avoid, but in a statically compiled binary, you know all the implementors, and you could enforce the unconstrained type parameters on implementors either way.
Probably solidly in the not-really-worth-it idea bin.
Yeah, that would be nice, but it is solidly in the not ānot-really-worth-it idea binā.