This is a brach off of Pre-RFC: Sealed traits, and has few new semantics and lots of new layout guarantees.
NOTE This Pre-RFC is CLOSED
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.
Guide-level explanation
There are two different proposed syntaxes,
#[seal]
trait SealedTrait {}
// or
seal trait SealedTrait {}
For this proposal I will use the keyword syntax.
Trees
Efficient trees can be defined quite easily with this feature.
seal trait Ast { eval(self) -> f32; }
struct Value(f32);
struct BinaryOp<L: Ast, R: Ast, F: FnOnce(f32, f32)>(F, L, R);
struct UnaryOp<V: Ast, F: FnOnce(f32)>(F, V);
impl Ast for Value {
fn eval(self) -> f32 { self.0 }
}
impl<L: Ast, R: Ast, F: FnOnce(f32, f32)> Ast for BinaryOp<L, R, F> {
fn eval(self) -> f32 { (self.0)(self.1.eval(), self.2.eval()) }
}
impl<V: Ast, F: FnOnce(f32)> Ast for UnaryOp<V, F> {
fn eval(self) -> f32 { (self.0)(self.1.eval()) }
}
This Ast
is can be defined entirely on the stack (no indirection required!) and runs as fast as an enum without any dynamic dispatch!
note: this section needs work, as I have realized that dyn Trait
cannot be Sized
due to generic parameters.
Error Handling
Another example use-case for this would be ergonomic error handling within a crate.
In crate A
seal trait CustomError {
fn message(&self) -> String;
fn error_pre(&self) -> &'static str;
}
impl CustomError for String {
fn message(&self) -> String {
self.clone()
}
fn error_pre(&self) -> &'static str {
"Lazy Error"
}
}
struct Timeout<T: Display>(T);
impl CustomError for Timeout {
fn message(&self) -> String {
format!("{} timed out!", self.0)
}
fn error_pre(&self) -> &'static str {
"Timeout"
}
}
impl<E: CustomError> Display for E {
...
}
fn get_component<C: Component>(timeout: u32) -> Result<C, dyn CustomError> {
let component = fetch_component_from_database(timeout);
if let Some(component) = component.get() {
if is_component_valid(&component) {
Ok(Component)
} else {
// look, easy error handling
// String will be coerced into
// dyn CustomError, just like
// any other trait
Err("Component is not valid".to_string()) // ad hoc error handling till later refactor
}
} else {
// look, easy error handling
// Timeout will be coerced into
// dyn CustomError, just like
// any other trait
Err(Timeout("Database"))
}
}
In crate B, which uses crate A
/*
struct NewError;
// Error, you cannot implment a sealed trait outside it's crate!
impl CustomError for NewError {
}
*/
fn foo() {
let player = crateA::get_component::<Player>(100);
let player = player.unwrap(); // no need to think of the error type here
// ...
}
Safety
todo
Reference-level explanation
Sealed traits are powerful, and come with a set of guarentees
- Sealed traits cannot be implemented outside of their crate
- Sealed traits can be implemented for any type
- removing some orphan rules because they don’t matter to sealed traits
- 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
- due to this,
Sem-Ver changes
- adding functions to sealed traits is not a breaking change
- making a trait sealed is a major breaking change
- this is because types that used to be able to implement the trait now can’t
- unsealing a trait is a major breaking change
- this is because any code relying on the fact that
dyn SealedTrait: Sized
will become invalid
- this is because any code relying on the fact that
Drawbacks
Expanding the language adds complexity and maybe a new keyword.
Rationale and Alternatives
This is good for Rust because it bridges the gap between enums and traits, and allows for an efficient middle ground. Thereby allowing Rust to show off more safe zero-cost abstractions.
- Not doing this
- One of the (currently) two enum expansion RFC would solve a small subset of the problems listed here
- Pre-RFC: Anonymous variant types which is now moved to github here
- Pre-RFC: sum-enums
Prior art
- Kotlin’s sealed classes
- Scala’s sealed traits
Unresolved Questions
- What syntax do we want, a new keyword or an attribute?