Skip to content

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

Closed
wants to merge 8 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
363 changes: 363 additions & 0 deletions text/0000-impl-enum.md
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.
Copy link
Contributor

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.

* It's also possible to use enums for generics additional to traits.
Copy link

Choose a reason for hiding this comment

The 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);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't it be arg: T? Doesn't make sense otherwise.

}
```

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 {
Copy link
Contributor

Choose a reason for hiding this comment

The 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?
If I didn't know that Event is a bound here I would interpret this as dependent typing...

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.
Copy link
Contributor

@Centril Centril Dec 28, 2018

Choose a reason for hiding this comment

The 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.