An alternative to enum variants types

I'm still finding some situations where it's useful to tell apart enum variant as distinct types. But recently I've seen that often a simpler feature suffices to me: a way to disallow some enum variants. A syntax could be:

Option - None
MyEnum - { A, C }

The {} syntax is used to disallow more than one variant.

A match{} on the values of those types must omit the branches on disallowed variants.

Those types have the same runtime representation of Option and MyEnum.

An .into() should be enough to coerce them to the original enum with all variants.

I've wanted enum-variant-as-a-nameable-type for a while (I have an issue comment somewhere…). I have some probing questions that should hopefully help flesh out this idea :slight_smile: .

  • How is name lookup done on the right side? Must it be MyEnum - { MyEnum::A }? What if it was MyEnum - { MyEnum::None } (where the name would be potentially ambiguous otherwise)? (This goes back to this previous thread.)
  • What is Option - { Some, None }? It should be a Never-like type (unless the type is #[non_exhaustive] I suppose).
  • Given type MySmallerEnum = MyEnum - { MyEnum::A };, is MySmallerEnum - { MyEnum::A } valid? How about MySmallerEnum::C (or must it still be MyEnum::C)?
  • Can this stack? Given type MyOtherSmallerEnum = MyEnum - { MyEnum::C };, is MySmallerEnum - MyOtherSmallerEnum possible? If so, is it the same type as MyEnum - { MyEnum::A, MyEnum::C } or are they distinct? If distinct, do I need a .into() for each "type subtraction" layer to get to the original?
  • Is u8 - { 0 } equivalent to NonZero<u8>?
  • What is the interaction with #[non_exhaustive]?

Anyways, related prior work:

1 Like

Disclaimer: I'm very ignorant of compiler internals and what the implications of any of this is for the compiler

I've often wanted the opposite. In your example, something like

Option::{Some}
MyEnum::{B, D}

The main use case in my mind is being able to make function signatures more explicit as to what enum variants they return, mainly when it comes to error enums. Making an error enum for each function is cumbersome, and every function returning a big enum with all the possible errors that can occur in a library loses some of the benefits that should come with an error enum.

I think explicitly listing the included variants is preferable, because knowing what MyEnum - { A, C } means requires knowing and remembering all the variants of MyEnum, and I feel this feature would be most useful with big enums where you're only returning a couple of variants. That said, I'd be interested to know what your use cases are.

My thoughts on the questions from @mathstuf:

  • How is name lookup done on the right side? Must it be MyEnum - { MyEnum::A } ? What if it was MyEnum - { MyEnum::None } (where the name would be potentially ambiguous otherwise)? (This goes back to this previous thread.)

Since the type of the enum is always included, this shouldn't be an issue.

  • What is Option - { Some, None } ? It should be a Never -like type (unless the type is #[non_exhaustive] I suppose).

It'd function the same as enum MyOption {}.

  • Given type MySmallerEnum = MyEnum - { MyEnum::A }; , is MySmallerEnum - { MyEnum::A } valid? How about MySmallerEnum::C (or must it still be MyEnum::C )?

If it's not necessary to include the type before each variant (it should not even be allowed, imo), this shouldn't be an issue. I don't think

type MySmallerEnum = MyEnum - A;
type EvenSmallerEnum = MySmallerEnum - A;

makes sense, since MySmallerEnum has no A variant to exclude, but I also don't think it would be harmful to allow it if it was simpler to implement like that.

  • Can this stack? Given type MyOtherSmallerEnum = MyEnum - { MyEnum::C }; , is MySmallerEnum - MyOtherSmallerEnum possible? If so, is it the same type as MyEnum - { MyEnum::A, MyEnum::C } or are they distinct? If distinct, do I need a .into() for each "type subtraction" layer to get to the original?

I don't think MySmallerEnum - MyOtherSmallerEnum should work. MySmallerEnum - { C } should be interchangeable with MyEnum - {A, C}

  • Is u8 - { 0 } equivalent to NonZero<u8> ?

I don't think this should be usable for anything but enums.

2 Likes

What about #[non_exhaustive] enum MyOption { Some, None }?

That's fair. At this stage, implementation of something like that seems trivial; I'm far more concerned about semantics and the effects. Disallowing it as an error means that adding an exclusion to one of my exported aliases of this is a breaking change for my API (since a consumer could have removed it in addition for their own further use).

Nice questions, thank you.

How is name lookup done on the right side? Must it be MyEnum - { MyEnum::A }? What if it was MyEnum - { MyEnum::None } (where the name would be potentially ambiguous otherwise)? (This goes back to this previous thread.)

I don't understand. How can it be ambiguous? The names on the right must be present in the enum/union on the left.

What is Option - { Some, None }?

A enum/union with no variants, an empty type,so it's like the Never type.

Given type MySmallerEnum = MyEnum - { MyEnum::A };, is MySmallerEnum - { MyEnum::A } valid?

Often with types the tidier the better, so it's statically disallowed to remove variants that are absent.

How about MySmallerEnum::C (or must it still be MyEnum::C)?

Removing MySmallerEnum::C is OK. See below.

Can this stack? Given type MyOtherSmallerEnum = MyEnum - { MyEnum::C };, is MySmallerEnum - MyOtherSmallerEnum possible?

They are structurally typed sets of variants (row typing of variants), so it's OK.

If so, is it the same type as MyEnum - { MyEnum::A, MyEnum::C } or are they distinct? If distinct, do I need a .into() for each "type subtraction" layer to get to the original?

The variants are a set, unsorted. And for this feature I prefer structural typing. So if they contain the same set of variants, they are the same type.

Is u8 - { 0 } equivalent to NonZero?

This isn't allowed. u8 isn't a enum/union. I prefer to do this with another syntax (slice syntax on integral types), that is more restricted (and more succinct) because it only allows contiguous intervals:

type NonZeroU32 = u32[1 ..];

What is the interaction with #[non_exhaustive]?

I don't know.

I'm not sure how the feature should interact with #[non_exhaustive]. The simplest solution would be to ignore it and say that enum subsets (or whatever they should be called) are always exhaustive. If there's a need for non-exhaustive subsets, maybe there should be a special syntax to indicate that (just as an example, MyEnum - {A, B, ..} or MyEnum::{A, B, ..}), separate from the exhaustiveness of the parent enum. I haven't thought this out too well though, maybe there are some awkward interactions that would result from this.

The way I figured it, they would have the same stability implications as enums when it comes to adding/removing variants. It would be a breaking change the same way if you removed the variant from the original enum. But this is another case where I'm not sure what the best functionality would be.

I see. There's no hope for a simple feature then, perhaps :slight_smile:

Acutally, maybe this just goes back to the syntax selection here. I may have a variable named MyEnum as well, so MyEnum - { None } may be a valid expression using subtraction (given an impl Sub<Option<T>> for TypeOfMyEnumVar).

This is an API hazard; I can no longer remove or add removed variants from my public API without affecting any further variant removal usages in any dependencies.

But is not allowed if they happen to remove any common subset of variants (due to the "cannot remove already removed variants" rule above?

This seems like an important detail to cover :slight_smile: .

Then you're in an API hazard situation. If it's always exhaustive, then what happens when the underlying #[non_exhaustive] enum adds a new variant? I didn't list it explicitly, so it goes through.

.. usually means "and all remaining", so I don't think it would work well for "and any unnamed variants that may appear in a future API version". Not sure how one can just erase the #[non_exhaustive] without wanting a way to forward it (since it was wanted to be able to preserve API compatibility over upgrades).

#[non_exhaustive] was the solution to this in the basic enum case. I don't see how it can't be considered here.

Right, I hadn't really considered the "exclusive" syntax before so I hadn't thought about that. I feel more strongly now that the "inclusive" syntax is preferable. Adding new variants is not an issue in that case. Even removing is fine in some cases, if the particular variant is not included.

The rationale for .. is from viewing it as the "and some other things, maybe" operator, as in

    struct S {
        a: u32,
        b: u32,
    }
    let s = S { a: 0, b: 0 };
    let S { a, b, .. } = s; // "non-exhaustive" destructuring

but I'm not attached to any particular syntax/solution for this.

Sure. Would the attribute be on the type or on the type alias? Or maybe either one? I'm wondering if it would still be possible to have a non-exhaustive subset "inlined", like

fn f() -> #[non_exhaustive] Enum::{A} {
    todo!()
}

or if being non-exhaustive meant that you would need a type alias. It would look pretty awkward in a real function with a longer signature, but I guess if anyone has a problem with that they can just extract it to a type alias.

Exactly. This proposal optimizes for writing at the expense of readability, which is almost always the wrong thing to do.

1 Like

There is a meaningful difference between the inclusive and subtracting variants for #[non_exhaustive]. The inclusive version is unaffected by it, whereas the subtracting version would naturally include the non-exhaustiveness. I can see legitimate use cases for both: e.g. an error handling function that handles a couple of related error cases, vs a function that handles all errors other than the ones that have already been handled.

Part of the RFC would be to motivate with real-world examples the variant that the author wishes to address.

Even if I accept this argument, it doesn't help with the readability problem.

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