[PreRFC] enum-variant-types

Summary

Allow enum variants to be their own types, as if they were seperate structs.

This means you can access them without the extra code of an entire match

or if let.

It would be like "This variable is of type SomeEnum, but only ever the Foo

variant."

Motivation

Why are we doing this? What use cases does it support? What is the expected outcome?

This is important because it allows for easier use of internal data structures

(eg: ASTs, IR/ILs, etc).

The outcome is that enum variants will be able to be destructed as if they were

all seperate structs.

Guide-level explanation

This section assumes familiarity with rust enums and ADTs

Consider the following enum:


enum SomeEnum {

Foo(i32),

Bar(&str),

Baz(u64)

}

Now, lets say you want to use just Foo(i32) for a certain function, but just writing

fn f(x: SomeEnum) doesnt ensure that unless you add extra code.

This feature allows you to write fn f(x: SomeEnum::Foo) and if you did:


fn f(x: SomeEnum::Foo) { ... }

f(SomeEnum::Bar("Hello, World!"))

it would error with something along the lines of:


error: function f only takes the variant `Foo`

--> src/main.rs

|

3 | f(SomeEnum::Bar("Hello, World!"))

| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ is variant Bar

|

This means making functions which take only one variant is easier, and as simple as

annotating it in the argument.

Similarly, it can also be done for the returns of functions.

Along with this, you can also use it to annotate variables:


let x: SomeEnum::Foo = SomeEnum::Foo(3);

This allows you to access the fields of SomeEnum::Foo as if its its own struct.

For example:


println!("{}", x.0); // prints 3

Notice how it's used the same as:


struct Foo(i32);

Finally, they can also be used in generics.


let mut y: Vec<SomeEnum::Foo> = vec![SomeEnum::Foo(3), SomeEnum::Foo(4)];

f(y.pop().unwrap());

You can also lower something of type SomeEnum::Foo to simply SomeEnum

dynamically. For example:


let x: SomeEnum::Foo = SomeEnum::Foo(5);

let y: SomeEnum = x;

(EVERYTHING UNDER HERE WILL PROBABLY CHANGE) Traits can be implemented to each

variant, and can be used in generics. For example:


impl SomeTrait for SomeEnum::Foo {

...

}

fn somefunc<T>(a: T)

where T: SomeTrait

{

...

}

Doing:


somefunc(SomeEnum::Baz(3));

will result with an error along the lines of:


error[E0277]: the trait bound `SomeEnum: SomeTrait` is not satisfied

--> test.rs:12:14

|

12 | somefunc(SomeEnum::Baz);

| -------- ^^^^^^^^^^^^^ the trait `SomeTrait` is not implemented for `SomeEnum::Baz`

| |

| required by a bound introduced by this call

|

However, doing:


somefunc(SomeEnum::Foo(5));

would work fine as the trait SomeTrait is implemented for that variant.

Similar to using an unwrap on Option<T> and Result<T, E>, you can also

use it on enums to get a specific variant. For example:


// same function as before

let x = some_random_variant();

f(x.unwrap()); // compiler inferrs the variant to be `SomeEnum::Foo`, and expands

// it to .unwrap::<SomeEnum::Foo>()

Notes on usage

The variant will attemt to be inferred through the use of constants of branching:


// constants

let var: SomeEnum = SomeEnum::Foo(32);

f(var); // not an error since var is assigned to a constant `SomeEnum::Foo`

// branching

let var: SomeEnum = some_external_function();

match var {

SomeEnum::Foo(x) => ...

_ => panic!()

}

f(var); // not an error (program exits if its not x)

All traits implemented to the entire enum will be inhereted by each variant.

Reference-level explanation

Implementing this will fit well with normal enums and other code, with no

breaking changes. All uses must be explicitly annotated by the user and doesnt

affect the current uses of enums.

To implement this you would need to first add analysis of the usage of enums,

to infer the variants. Some possible approaches to this can be branching analysis

and constants. Enum variants would also need to become first-class types, and

allowing them to be lowered into just SomeEnum automatically would need to be

added.

Drawbacks

Why should we not do this?

  • Adds complexity to the type system in general
  • Requires extra analysis of prior-code

  • If not done at compile time, it adds overhead to the language

  • Analysis can detract from the ergonomics (the compiler cannot know everything

about the program)

  • Possible solution: some_enum.unwrap::<SomeEnum::Variant>()

Rationale and alternatives

This design is the best because currently you have to use pattern matching to

ensure an enum is the right variant, unwrap the values inside, etc, further

making rust easier to write.

It also further enhances readability by making it clear what variants it takes,

and enforces it at compile time.

Alternatives:

  • Refined types

Prior art

Unresolved questions

  • #[derive] on different variants?

  • a possible someenum.unwrap::<SomeEnum::Variant>() could be added?

Future possibilities

Makes the language more suitable for data analysis, programming language dev,

etc.

Makes enums more ergonomic to use.

3 Likes

discourse kinda stuffed up my indentation, sorry

Why just not use special type for this?

pub struct EnumBarVariant { ... }
impl EnumBarVariant {
    pub fn from_enum(value: Enum) -> Option<Self> {
        match value {
            Enum::Good(var) => Some(Self { ... }),
            _ => None
        }
    }
}

Just make your own impls for required traits.

What I like about your idea is limitation of allowed parameters for function. But what about several enum variants (Maybe + or other syntax, but is different variants are different types)? What about runtime (maybe use match for ensure)?

As for me its better to have special type instead of kind of specialization over enums (maybe this is good solution?)

2 Likes

Why not use a special type for this?

First-class language support makes it much more ergonomic to use than copying and pasting the entire thing, calling from_enum and whatnot. You also need the boilerplate of an entirely new struct. It also can be done at compile time through typechecking with my proposed RFC, hence making it faster.

Function limitations

Yeah, I was thinking about something like that Here are some of my ideas:

fn foo(x: SomeEnum::Foo | SomeEnum::Bar, y: !SomeEnum::Bar, z: impl SomeEnum: ToString);

X can be Foo or Bar, Y cant be bar, and the variant for Z has to implement ToString If you wanted to do it at runtime, I think the only way is to use match, otherwise its like passing a u32 to a function that takes a char

Needs more thought though.

Thanks for your feedback :smiley:

Important priot art: https://github.com/rust-lang/rfcs/pull/2593.

1 Like

Thanks, I'll add it now

A more general version of this feature has been discussed under the name "pattern types".

5 Likes

Thanks, I'll add it in the prior art section.
I still think this feature should be implemented as pattern types don't really let you do some of the features listed in the RFC, such as the ability to destruct the enum variants as if they were its own struct, and the idea that traits can be implemented for specific variants. They would work nicely together though, as I'd imagine most of the code can be reused.

Do you guys think I’m good to open the PR on the RFCs repo in it’s current state?

Please give more details on that. It seems like pattern types could do all of those things. I think we might be able to support implementing traits on a pattern type, for instance. (That may be tricky in terms of ambiguity, because it's not clear which impl Some(x).method() should call, but I think that applies to your proposal as well.)

3 Likes

I'm a huge fan of the pattern types idea because I think it gives a very reasonable explanation of the types involved, and how they're chosen, in a way that I don't think I've ever seen in detail for just enum variant types.

As a trivial example,

let foo: Option<i32> = ...;
match foo
    Some(w @ 10..20) => /* here `w` has type `i32 is 10..20` */
    x @ Some(_) => /* here `x` has type `Option<i32> is Some(_)` */
    z @ None => /* here `z` has type `Option<i32> is None` */
}

But it also directly allows things that don't work with just single-variant enum types, like

fn foo(x: std::cmp::Ordering is (Less | Greater)) {
    match x {
        .Less => ...,
        .Greater => ...,
        // Nothing required for `Equal`
    }
}

Or, phrased as direct feedback on this pre-RFC, I think the Reference-level explanation is currently insufficient.

It should very precisely define exactly what the rules are for when a variable is considered to be of a variant type, rather than the full type.

It should also include the convertibility rules, and the implications of those on the layouts of enum variant types.

4 Likes

I think there should be some mention here of "flow typing", either as first class inferred types or as a future possibility. The RFC as written seems to be in favor of only explicit annotations,

but that is skipping a decent amount of detail. As a simpler example:

let some = Option::<i32>::Some(2);
// what is the type of `some` here?

As @scottmcm is hinting the exact syntax for how we name this type (even if it is open to bikeshedding) is important here. The above code compiles today.

Thanks for all the feedback, I will update the RFC accordingly. I think we should go for a combination of both of RFCs, where the pattern types can allow for enums to be destructed in the way described in this RFC. The syntax for limiting function arguments should be kept the same too.
One thing I cant decide is the issue @toc raised, and if some would be Option<T> or Option<T>::Some(T). I'm currently leaning towards it being just Option<T> by default.

The RFC isn't about your leanings, it's about writing down all the details, linking all the relevant resources and discussion, listing out potential tradeoffs, providing a basis for new discussion and insights, and yes ultimately proposing a path forward (which might be changed or rejected via discussion, who knows). Don't look at the comments in this thread as things to decide between, it's all detail that needs to be fleshed out in the body of the RFC. And this topic you have picked needs a lot of fleshing out.

As far as it being Option<T> by default, of course it has to be Option<T> in this case; the code I wrote already compiles and that's the type. Pattern types must necessarily be some refinement of existing types to work at all. That's part of what @scottmcm is expressing in his example.

let foo: Option<i32> = ...;
match foo
//                                            vvvvvvvvvvvvv
    Some(w @ 10..20) => /* here `w` has type `i32 is 10..20` */
//                                       vvvvvvvvvvvvvvvvvvvvvv
    x @ Some(_) => /* here `x` has type `Option<i32> is Some(_)` */
//                                    vvvvvvvvvvvvvvvvvvv
    z @ None => /* here `z` has type `Option<i32> is None` */
}

The phrasing is not an accident, and while the exact syntax is up in the air the answer to my question should be something like:

let some: Option<i32> is Some(_) = Option::<i32>::Some(2);

What are the detailed implications of this?

2 Likes

One extra thing I'll emphasize here is that this is arguably really about subtyping, which means that as soon as something in this direction happens there's a ton of "obvious" things that people will ask for, and thus a strong RFC will sketch a vision for how those things could look, even if they're not done right away, as part of describing why the proposed approach is a good one.

For example, I might want to write something like this:

impl<T: Clone> Iterator for iter::Repeat<T> {
    #[refine]
    fn next(&mut self) -> Option<T> is Some(_) {
        Some(self.0.clone())
    }
}

in the spirit of 3245-refined-impls - The Rust RFC Book, so that if I know I have a Repeat, I'd be able to do let Some(x) = it.next(); -- no else { unreachable!() } needed -- because the refined impl promised to the irrefutable pattern checker that None can't happen.

This touches the type system pretty fundamentally -- even if just for single variants -- and thus it's a huge rabbit hole.

9 Likes

I'm making a future proposal Partial Types v3

And exists topic on Internals - [Pre? RFC] Partial types and partial mutability

And Partial Enums looks like this:

fn f(x: SomeEnum.{Foo}) { ... }

let x: SomeEnum.{Foo} = SomeEnum::Foo(3);
1 Like

Looks like my RFC but better, good job! Will help if you need it My discord is emm312

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.