-
Notifications
You must be signed in to change notification settings - Fork 1.6k
RFC: Using enums like traits #2618
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9f7cda5
eaa7d7f
9d86d3e
4e30912
76691d5
ea58224
ddece45
11e88c6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,363 @@ | ||
- Feature Name: impl_enum | ||
- Start Date: 2018-12-25 | ||
- RFC PR: (leave this empty) | ||
- Rust Issue: (leave this empty) | ||
|
||
# Summary | ||
[summary]: #summary | ||
|
||
This is a proposal for a new interpretation of enum types, so they can get stored as their variant types and matched at compile time. | ||
This simplyfies writing efficient generic code when using enums. | ||
|
||
In the following this enum will be considered: | ||
|
||
```rust | ||
enum Enum { | ||
Variant1, | ||
Variant2(...), | ||
Variant3{...}, | ||
... | ||
} | ||
``` | ||
|
||
Now enums implicitly generate structs for each variant like this: | ||
|
||
```rust | ||
mod Enum { | ||
pub struct Variant1; | ||
pub struct Variant2(...); | ||
pub struct Variant3{...}; | ||
... | ||
} | ||
``` | ||
|
||
That's not the main feature of this proposal, but necessary to get it work. | ||
|
||
Additinally, enums generate a type similar to traits for the enum, which are implemented by the structs, and also work in a similar way. | ||
The current enums are the counterpart to trait objects then. | ||
|
||
# Motivation | ||
[motivation]: #motivation | ||
|
||
Let's begin with an example: | ||
|
||
```rust | ||
use recieve_exit_event; // fn() -> bool | ||
|
||
trait Updateable<E> { | ||
fn update(&self, event: E) -> bool; | ||
} | ||
|
||
enum Event { | ||
Idle, | ||
Exit, | ||
} | ||
|
||
struct Object; | ||
impl Updateable<Event> for Object { | ||
fn update(&self, event: Event) -> bool { | ||
match event { | ||
Event::Idle => true, | ||
Event::Exit => false, | ||
} | ||
} | ||
} | ||
|
||
fn run() { | ||
let current_updatable = (&Object as &dyn Updateable<Event>); | ||
loop { | ||
if recieve_exit_event() { | ||
if !current_updatable.update(Event::Exit) { | ||
break; | ||
} | ||
} | ||
if !current_updatable.update(Event::Idle) { | ||
break; | ||
} | ||
} | ||
} | ||
``` | ||
|
||
This is a small implementation of an event loop. Inside the loop, new events are generated under certain conditions. Then they are passed to a method, which handles these events. | ||
|
||
The event types will probably contain some information important for the events. | ||
|
||
The event types are known at compile time, so it would be more efficient to write it like this: | ||
|
||
```rust | ||
use recieve_exit_event; // fn() -> bool | ||
|
||
trait Updateable<IdleEvent, ExitEvent> { | ||
fn update_idle(&self, event: IdleEvent) -> bool; | ||
fn update_exit(&self, event: ExitEvent) -> bool; | ||
} | ||
|
||
mod Event { | ||
pub struct Idle; | ||
pub struct Exit; | ||
} | ||
|
||
use Event::* | ||
|
||
struct Object; | ||
impl Updateable<Idle, Exit> for Object { | ||
fn update_idle(&self, event: Idle) -> bool { | ||
true | ||
} | ||
fn update_exit(&self, event: Exit) -> bool { | ||
false | ||
} | ||
} | ||
|
||
fn run() { | ||
let current_updatable = (&Object as &dyn Updateable<IdleEvent, ExitEvent>); | ||
loop { | ||
if recieve_exit_event() { | ||
if !current_updatable.update_exit(Event::Exit) { | ||
break; | ||
} | ||
} | ||
if !current_updatable.update_idle(Event::Idle) { | ||
break; | ||
} | ||
} | ||
} | ||
``` | ||
|
||
This new approach will not create enum types just to match them directly afterwards, when it's still known, and removes a little unnecessary overhead. | ||
|
||
But this comes with problems. When adding a new event type, it has to be added in many places instead of just once in the enum declaration and once in the `match` expression using the enum. Because it's an enum, the compiler will even warn, if you forget to add some branch, but not if you forget adding a new method, so this small performance benefit is most likely not worth it. | ||
|
||
But this means, the helpful abstraction using enums is not a zero cost abstraction in this scenario. | ||
|
||
`impl enum` will automatically define the more efficent code without losing the abstractions. | ||
|
||
|
||
# Guide-level explanation | ||
[guide-level-explanation]: #guide-level-explanation | ||
|
||
As told in the summary, an enum works like this: | ||
|
||
```rust | ||
mod Enum { | ||
pub struct Variant1; | ||
pub struct Variant2(...); | ||
pub struct Variant3{...}; | ||
... | ||
} | ||
``` | ||
|
||
There are still a few differences. Since enum struct fields are public by default, the fields of these structs have to be public by default, too. | ||
|
||
These variant structs implement the enum, similar to how other types implement traits. | ||
|
||
Enum types can just be used like trait types now: | ||
|
||
* It's preferred to add a `dyn` modifier, when using an enum directly. | ||
* It's also possible to use enums for generics additional to traits. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What does this exactly mean? |
||
* Even using `impl Enum` is possible. | ||
|
||
Like for `impl Trait`, using `impl Enum` as a return value will require all possible return values to be of the same enum variant of the specified enum. | ||
|
||
## How matching works | ||
|
||
Matching variant structs works different to matching normal structs: | ||
|
||
```rust | ||
struct Struct1; | ||
struct Struct2(...); | ||
|
||
match Struct2(...) { | ||
Struct1 => ..., | ||
... | ||
} | ||
|
||
if let Struct2(...) = Struct1 { | ||
... | ||
} | ||
|
||
while let Struct2(...) = Struct1 { | ||
... | ||
} | ||
``` | ||
|
||
None of these examples will work using normal structs, but variant structs work different. | ||
When two structs implement the same enum, only the matching branch stays. All other branches are removed at compile time. | ||
|
||
This already works, when not knowing the exact type, so writing generic functions, specialized on a single type, is easy. | ||
|
||
Whereever required, enums are implicitly cast to dynamic enums, for example when calling a function, that takes a dynamic enum. This is required to stay backwards compatible. Casting back to enum variants directly is not possible. Match should be used for that, just as before. | ||
|
||
## Enums in traits | ||
|
||
Since enums cannot be extended after definition, it's allowed to make traits, which contain generic functions, into objects, in case the generics are required to be enums: | ||
|
||
```rust | ||
trait Trait { | ||
fn test<T: Enum>(&self, arg: Enum); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't it be |
||
} | ||
``` | ||
|
||
The vtable of this trait will contain a new entry for every variant of the enum type. In case of multiple enums as arguments, it will just generate even more entries. Using multiple generic types for traits, that will be made into an object, should be avoided. | ||
|
||
It may be useful to define a trait, specialized for multiple different enums, to generate associated traits. This will look like this: | ||
|
||
```rust | ||
trait Trait { | ||
enum Enum; | ||
fn test<T: Self::Enum>(&self, arg: T); | ||
} | ||
``` | ||
|
||
## Example | ||
|
||
|
||
Using these new feature, writing an efficient version of the motivational example is now possible: | ||
|
||
```rust | ||
use recieve_exit_event; // fn() -> bool | ||
|
||
// remove generic type from | ||
trait Updateable { | ||
// add associated enum | ||
enum Event; | ||
// require associated enum | ||
fn update<E: Event>(&self, event: E) -> bool; | ||
} | ||
|
||
enum Event { | ||
Idle, | ||
Exit, | ||
} | ||
|
||
struct Object; | ||
impl Updateable for Object { | ||
// update implementation to match new version of Update | ||
enum Event = Event; | ||
fn update<E: Self::Event>(&self, event: E) -> bool { | ||
match event { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems to me that some sort of type-case is going on here... could you elaborate? |
||
Event::Idle => true, | ||
Event::Exit => false, | ||
} | ||
} | ||
} | ||
|
||
fn run() { | ||
let current_updatable = (&Object as &dyn Updateable<Event = Event>); | ||
loop { | ||
if recieve_exit_event() { | ||
if !current_updatable.update(Event::Exit) { | ||
break; | ||
} | ||
} | ||
if !current_updatable.update(Event::Idle) { | ||
break; | ||
} | ||
} | ||
} | ||
``` | ||
|
||
As it's easy to see, with this feature, there are almost no differences in the implementation, but the overhead from using enums is eliminated. | ||
|
||
|
||
# Reference-level explanation | ||
[reference-level-explanation]: #reference-level-explanation | ||
|
||
It seems the explanation in the previous part is already detailed enough for most parts and moving parts down here will just make it less clear. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not at all. It's not even clear at a high level from the guide what this does let alone what impact it would have on Rust's static semantics, dynamic semantics, and syntax. |
||
|
||
## Enums and traits | ||
|
||
Enums and Traits can both be used as requirements for generic type arguments in the same way, but only one enum dependency is allowed. Else it may not be clear, how `match` works for this type. | ||
|
||
# Drawbacks | ||
[drawbacks]: #drawbacks | ||
|
||
It's a big extension and may have little advantages. | ||
|
||
# Rationale and alternatives | ||
[rationale-and-alternatives]: #rationale-and-alternatives | ||
|
||
## Constant generics | ||
|
||
Just having constant generics could also help here. Enums would just be used as constant generic parameters. Anyway, this would not be as powerful. | ||
It will not allow to turn traits with generics parameters in the methods into trait objects. | ||
Also, not only the `match` will be resolved at compile time, the arguments for the events also have to be constant. | ||
|
||
Using const generics, the `Updatable` trait could look like this: | ||
|
||
``` | ||
trait Updateable { | ||
fn update<E: const Event>(&self) -> bool; | ||
} | ||
``` | ||
|
||
## Const enum | ||
|
||
The first approach was using `const enum`. This would work just like an enum: | ||
|
||
```rust | ||
const enum Enum {...} | ||
``` | ||
|
||
When defining an enum like this, all types of this enum are just structs, and using the name of the enum type works like an `impl Trait` and implicitly generates specialized versions. The problem is, it's less clear, when specialized versions are generated implicitly. | ||
|
||
## Implicitly converting enum types to supertypes everytime | ||
|
||
Another idea was to implicitly convert enum variant types to enum types, after creation. | ||
|
||
This would ensure backwards compability, too, and would not add some implicit compile time optimizations in case a match is used on the created type. | ||
|
||
In this version, the following expressions are just the same: | ||
|
||
```rust | ||
let value = EnumName::Variant1; | ||
``` | ||
```rust | ||
let value: EnumName = EnumName::Variant1; | ||
``` | ||
|
||
But it would also be possible to do this, which is the only way to create specialized structs: | ||
|
||
```rust | ||
let value: EnumName::Variant1 = EnumName::Variant1; | ||
``` | ||
|
||
But since using enum variants as constructors is just create dynamic enums, this would have to be cast back to an enum variant implicitly. | ||
|
||
So this syntax could be handled as a special case instead, but this would be confusing. | ||
|
||
It's also not clear, how this would work or why this would not work: | ||
|
||
```rust | ||
let value: EnumName::Variant2 = EnumName::Variant1; | ||
``` | ||
|
||
|
||
# Prior art | ||
[prior-art]: #prior-art | ||
|
||
It seems the RFCs #1450 and #2593 have similar goals. | ||
|
||
#1450 is not compatible with this RFC, because it's not fully backwards compatible in the intended way. | ||
|
||
#2593 is basically, what this RFC is about, but without the generic features. | ||
|
||
# Unresolved questions | ||
[unresolved-questions]: #unresolved-questions | ||
|
||
Will implicit casts work in a backwards compatible way? | ||
|
||
Should it be allowed to implemnet methods and traits for variant structs? | ||
|
||
Are variant types really public types at all? | ||
|
||
Is it possible to access struct fileds from variant structs directly? | ||
|
||
# Future possibilities | ||
[future-possibilities]: #future-possibilities | ||
|
||
When introducing associated enums, associated traits may also be useful, just for completeness. It shouldn't be a big deal, since they work pretty similar to associated enums, and even have less features. | ||
|
||
When there are types for the variant struct anyway, it would be nice to add a way to extract the struct type directly instead of just the contents. This would solve another thing, people were already interested in. | ||
|
||
It may be possible to add structs to multiple enums at once, but it's difficult to imagine a syntax, that does not forbid extending enums after creation. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not clear what this entails from the phrasing.