Trait-based enum Variants
I think allowing Trait implementations to be passed around like manually defined enums would be a useful feature and fits really well into Rusts core principles of Zero cost abstractions and allowing the developer to choose what they want to use and how they want to store/pass around data. This would mean better cache locality, faster method calls (just choosing based on the enum) and potentially even inlining and other optimizations. Manually defining enums isn't always possible or ergonomic (see below) and we have to fall back to dynamic dispatch, while also loosing the ability to match on those (non-exhaustive) enums.
Proposal
Add additional syntax, another keyword (or use enum
), a macro-like annotation or similar to a trait or Type (see examples below), which indicates to the compiler that this shouldn't be a impl MyTrait
or dyn MyTrait
but that the compiler should (when compiling the final binary or dynamic/static library (basically when no new types can be added) create a new enum containing one Variant for every type implementing MyTrait
and use that, with an implementation of MyTrait calling the underlying variants Trait implementation (similar to how a dyn MyTrait
works from the callers perspective).
This enum type would be sized if the MyTrait is sized, would have a size known at compile time.
Examples and Potential syntax
trait MyTrait {
fn hello();
}
impl MyTrait for MyStruct{ /*...*/ }
fn example1(data: enum MyTrait) {
data.hello() // Forwarded to the respective variant
}
fn example2() -> enum MyTrait {
// Creation can be like any other struct is defined, automatically getting converted to the respective Variant of the (anonymous) enum
MyStruct{}
}
fn example3(data: enum MyTrait) {
match data {
MyStruct => todo!(),
String => todo!(),
MoreComplexType{a, b, c} => todo!(),
CompelxTypeWithGeneric<String>{s, ...} => todo!(), // This could be the same syntax as when specifying generics for a function
CompelxTypeWithGeneric<_>{a,b,c,..} => todo!(), // Ignore the type
// By its nature every dependency with access to MyTrait can add new types and thus variants, so this must be non-exhaustive everywhere (except for in binary crates)
_ => todo!(),
}
}
fn example4(data: &mut enum MyTrait);
struct {
data: enum MyTrait,
// Example of dyn (I don't know of the top of my head if Box is needed if MyTrait itself is sized)
dynamic: Box<dyn MyTrait>
}
Open Questions
- It might be possible that the creation of a new enum type (or its size or similar) could lead to other types having a valid Trait implementation and thus needing to be added to another such Auto-Enum
- The compiler might (internally) have some issues with types where their size and potentially alignment depend on other types it might not have seen, yet.
- Is it possible to automatically choose the discriminant size (unless specified)?
Motivation
Let's say we have a library or framework, which contains a (sized) Trait for some kind of data that can exist in multiple forms. Normally you'd use an enum for this, but the library/framework should be generic and work with new types created by user (binary or other library crate). Alternatively we could use Box<dyn MyTrait>
, paying the cost of an indirection and at the very least a pointer of 4 or 8 bytes, in addition to a second pointer for the underlying type and its methods. For most cases this is absolutely fine, but it does have a bad feeling to it - wasting those 4-8 bytes + type information where a single u8 would be sufficient if it was an enum.
We can also use Generics (or GATs if we have too many) to let the user define an enum in his crate, which he can pass in in the type or function call. This achieves the goal of being efficient in terms of memory size and potentially allows more optimizations than Box<dyn MyTrait>
. It does however have four downsides:
- The user has to define this enum - This could be solved or made easier by providing a macro doing this
- The user has to specify this enum type wherever he wants to use the library/framework and the compiler can't infer it from other places, thus leading to some boilerplate code, both in the library and in the users crate - It isn't really ergonomic but doable
- Let's say the user isn't the only one using this library, he is providing another library (think about the diamond pattern in multi-inheritance), where a third party uses both libraries. Let's say the second library requires some variants on this type to exist for it to even function. Now the second library would need to provide those generics, too and the outer-most user will need to write an enum satisfying all the dependencies, while also potentially having to deal with conflicting variant names.
- If the library depends on one variant being available and doesn't want to require yet another function on the enum just for creating his enum variants, as he cannot do anything with the enum directly since he isn't in control of it.
So we currently have the following options (unless I'm missing something):
Box<dyn MyTrait>
: Performance and memory size downside, straying from the Zero Cost Abstractions Rust is famous for.- Enum in library: Not extendable, also see for a similar feature request/question Do we have any proposals on extensible enums?, doing this with extendable enums could be possible, too
- Generic for this type everywhere (or bundled into a single trait with GATs): Some boilerplate and the above mentioned problem of not being able to use it as an enum (since it could also be a struct), thus this is basically mainly useful for stroring some user data
A more specific example
A library that works with multiple database backends (granted, the performance overhead of Box<dyn MyTrait>
isn't too big in this example, but this could also be something that's called a lot more often). Each backend provides a data type the library has to store and work on, all of which implement a trait defined by some generic database crate. Without any of the backends knowing about this feature, the library could provide an interface using an enum of all possible backend implementations and use that enum type (and the methods defined on the trait) internally, potentially even without the user of the library noticing. While at the same time not needing to use dynamic dispatch (as it's an enum). A user could use this libraries functionality with a custom/new database backend without the library providing a feature flag for (or even knowing about) this backend, still without requiring dynamic dispatch.
Similar Questions I've found
- Representing closed trait objects as enums - #19 by Ixrec - While this couldn't be used for automatic creation of the return types (due to needing a Trait), doing it this way could also solve some of the issues with breaking changes due to adding an additional (previously non-existing return type).
- Ideas around anonymous enum types - #2 by Ixrec - By defining all variants of this "anonymous enum" (trait-based enum) by some type implementing a trait, the name of this type (or rather it's type/path) can be used for pattern matching, similar to how they're used in destructuring structs in patterns. @Ixrec, you might be interested in this (sorry for the ping). With a private trait it would basically allow anonymous enums as linked above, although requiring an additional Trait and a few impl blocks. It does however not completely get rid of the pattern matching issue, for example when implementing
MyTrait
for a non-struct like u64.
I hope I haven't lost you, I'm curious if this would even be possible (probably would require a bunch of internal changes in the compiler since it's potentially adding a new type (or at least type variant) in any impl block.
Please let me know what you think of this idea, and if you could see it being a useful alternative to Box<dyn MyTrait>
.