Skip to content
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

Support Private Enum Variants #3506

Open
kevincox opened this issue Oct 6, 2023 · 11 comments
Open

Support Private Enum Variants #3506

kevincox opened this issue Oct 6, 2023 · 11 comments

Comments

@kevincox
Copy link

kevincox commented Oct 6, 2023

There should be a way to mark enum variants as private.

Possible syntax:

enum MyThing {
  priv OneState(Foo),
  priv OtherState(Bar),
}

Motivation

Type authors should be able to use an enum to represent their type without exposing that to users. Today if a type is an enum that is visible to users so almost any change is a breaking change (I think only adding an additional private field to a struct-style variant is not breaking).

For example if the leading example was written today without priv a user would be able to do:

match v {
    MyThing::OneState(..) => true,
    MyThing::OtherState(..) => false,
};

Possible Alternatives

#[doc(hidden)]

enum MyThing {
  #[doc(hidden)] OneState(Foo),
  #[doc(hidden)] OtherState(Bar),
}

This hides it from documentation but doesn't actually protect against using it. It is possible that the user copies an old code sample, guesses a name without knowing it is private or an IDE autocompletes it and the user will unknowingly be relying on a (logically) private API.

Internal Enum

enum MyThingImpl {
  OneState(Foo),
  OtherState(Bar),
}

pub struct MyThing(MyThingImpl);

This works but results in tedious code. Internals must be accessed as self.0 and if you want to derive traits this needs to be done on both the wrapper and the impl.

Related: #2028 rust-lang/rust#32770

@nikomatsakis
Copy link
Contributor

nikomatsakis commented Oct 19, 2023

My preference: In a future edition, make enum variants private by default. There is also however the challenge of tuple structs. I think pub Struct(T) should probably make the .0 field public by default (in contrast to today), for consistency.

I would probably want to do this:

  • () "inherits" public/private
  • {} defaults to private

in all contexts. This means that e.g. trait Foo { fn item() } should be trait Foo { pub fn item() }

@Yokinman
Copy link

Yokinman commented Mar 8, 2024

I feel that privacy goes contrary to the main advantages of enums:

  1. Enums only represent valid states.

    This contrasts with structs where you're often relying on primitives that express too much, and so constructors are used to validate or implicitly map onto valid states. If all possible states of a type are valid, it shouldn't matter that you can construct the type arbitrarily; there's no undefined behavior.

  2. match forces exhaustive handling of all possible states.

    If an enum has private variants, every match over it has to resolve to a catch-all _ case. Users won't know if there's new cases to handle when they update a dependency until it appears at runtime by chance.

    I'm even skeptical of the #[non_exhaustive] attribute. Like, if I'm trying to exhaust a value of type syn::Expr, I'd want to be warned at compile-time if a new expression was added in an update. If not, I'd add the _ case. I'm not sure, it just seems overused.

  3. Enums can form categories of types; similar to traits, but in a strict way where all variants are known and you have greater case-by-case control.

    I don't think privacy would really hinder this, but it's a good choice if privacy is desired. It's not that different from having private fields - which feels like an argument for private fields, but then you might want constructors specific to each variant. I think it's nicer to have that organized into separate types.

  4. Enums are convenient and readable. This is kind of an extension of point 1, but I think it's true that people reach for enums specifically because they provide a very clean way to construct, organize, & handle states.

    An enum with private variants seems like a weird half-struct, where you gain the ability to validate how certain variants are constructed, but lose half of their utility. In your example, Foo and Bar can validate their own states instead.

Generally: enums are explicit and direct, structs are implicit and indirect - I think it's nice to have that distinction.

Your friend,
Yokin

@Yokinman
Copy link

Yokinman commented Mar 8, 2024

Trying to think of a better alternative. Maybe variants could be allowed to pseudo-alias a type that they also wrap?

pub enum Expr {
    Array: ExprArray,   // Array(ExprArray)
    Assign: ExprAssign, // Assign(ExprAssign)
    Async: ExprAsync,   // Async(ExprAsync)
    // ..
}

? Functions and constructors accessed through the variant's path could be syntax sugared:

assert_eq!(Expr::Array::from([1, 2]), Expr::Array(ExprArray::from([1, 2])));

? Matching could be syntax sugared:

match text {
    Expr::Array { .. } => {},
    Expr::Assign { .. } => {},
    Expr::Async { .. } => {},
}

to

match text {
    Expr::Array(ExprArray { .. }) => {},
    Expr::Assign(ExprAssign { .. }) => {},
    Expr::Async(ExprAsync { .. }) => {},
}

This is basically what exists already, just a little less repetitive. Maybe it would be too confusing since trying to access a variable after construction would require pattern matching.

Alternatively, it could be interesting if each variant counted as its own type and you could implement associated functions and constants per variant (no methods), but I'm sure it's been proposed before. Maybe that would make them seem too type-like when you can't really do anything else with them, like implement traits or use them in function parameters.

Your friend,
Yokin

@SOF3
Copy link

SOF3 commented Mar 8, 2024

@Yokinman what about we look at this from another way, that enums with private variants are equivalent to #[non_exhaustive] ones? As for enums with only private variants, they are basically syntactic sugar for newtype structs wrapping a private enum.

Just as enums indeed should not add a #[non_exhaustive] for no reason, structs should not add a _private: () field no reason. The same argument about "exhaust a value of an enum" applies to "construct a value of a struct" too — if I'm trying to construct a struct, I would like to be warned at compile time if a new field appears, not through hiding an optional parameter in a constructor. The abuse of non-exhaustive enums is not much different from the abuse of private struct fields.

@SOF3
Copy link

SOF3 commented Mar 8, 2024

As far as your arguments would concern, the enum

pub enum MyThing {
    Foo(i32),
    Bar(i32),
    priv Baz(i32),
    priv Qux(i32),
}

is functionally equivalent to the current-stable syntax

pub enum MyThing {
    Foo(i32),
    Bar(i32),
    Underscore(Underscore),
}
pub struct Underscore(Inner);
enum Inner {
    Baz(i32),
    Qux(i32),
}

except 4 lines shorter and less troublesome to handle, and your argument is basically saying that we should always use a struct and make the syntax 4 lines longer to reduce chances of people doing this.

@Yokinman
Copy link

Yokinman commented Mar 8, 2024

I'm not super against enum privacy in terms of fields, but if it existed I think there should be a way to define "write" privacy independently from "read" privacy. So you can match on a variant, but you can't necessarily construct that variant. I think that could definitely be useful.

pub enum MaybePrime {
    Yes(&priv u64),
    No(&priv u64),
}

impl MaybePrime {
    pub fn new(num: u64) -> Self {
        if num.is_prime() {
            Self::Yes(num)
        } else {
            Self::No(num)
        }
    }
}

// ..in another crate..

fn num_value(num: MaybePrime) -> u64 {
    let (MaybePrime::Yes(x) | MaybePrime::No(x)) = num; // Allowed
    x
}

fn make_prime(num: u64) -> MaybePrime {
    MaybePrime::Yes(num) // Not allowed
}

I find it harder to think of an example where you might want fully private/inaccessible variants, other than specifically #[non_exhaustive]. I think it would be too easy to do out of convenience without realizing that you've neutered match.

Your friend,
Yokin

@senekor
Copy link

senekor commented Mar 8, 2024

I'm even skeptical of the #[non_exhaustive] attribute. Like, if I'm trying to exhaust a value of type syn::Expr, I'd want to be warned at compile-time if a new expression was added in an update.

This is not meant as an argument in the discussion, but you can kinda solve this problem today for yourself with the clippy lint wildcard_enum_match_arm. Example:

// lib.rs
#[non_exhaustive]
pub enum Foo {
    Bar,
    AddedLater,
}

// main.rs
#[warn(clippy::wildcard_enum_match_arm)]
match foo {
    Foo::Bar => todo!(),
    _ => todo!(),
}

The compiler forced us to add the wildcard before Foo::AddedLater was created. But clippy actually warns us now that the wildcard pattern matches something. Without a strong opinion on the topic, this is just a little trick that met my needs in the past.

@tryoxiss
Copy link

I think this is a bad idea, as it forces non-exaustive matching -- which removes a large part of the point of enums. Like Yokinman mentioned, having some way to not allow users to construct certin variants could possibly have merit, but entirely private fields makes no sense to me.

@kevincox
Copy link
Author

I disagree. Just because a client of a type shouldn't construct it doesn't make me want to remove the ability of the source of a type to construct it and benefit from sum types in general. It feels a bit baby-with-the-bathwater to not support some valuable features of enums just because one feature is undesired.

@Yokinman
Copy link

Yokinman commented Mar 23, 2024

I disagree. Just because a client of a type shouldn't construct it doesn't make me want to remove the ability of the source of a type to construct it and benefit from sum types in general. It feels a bit baby-with-the-bathwater to not support some valuable features of enums just because one feature is undesired.

I've paid more attention since I last posted and I've noticed that it's pretty useful to use enums for iterators that switch between multiple modes (surely other kinds of modal things too). You don't want it to be constructable by a user since you might change it later, but you do want to use it in a public return value. Maybe the enum could be marked with an attribute that makes all of its sub-variants private/unconstructable or something?

EDIT: Actually, I suppose you can do this already by stuffing a public enum into a private module. It's not intuitive, but it does achieve the function of making the enum inaccessible, yet still usable as an associated type / return type. Or maybe not, since you can still construct it through the associated type.

I don't think individually private variants are a good idea, but I like the sound of all variants being private at once.

Your friend,
Yokin

@tryoxiss
Copy link

tryoxiss commented Apr 5, 2024

I disagree. Just because a client of a type shouldn't construct it doesn't make me want to remove the ability of the source of a type to construct it and benefit from sum types in general. It feels a bit baby-with-the-bathwater to not support some valuable features of enums just because one feature is undesired.

I've paid more attention since I last posted and I've noticed that it's pretty useful to use enums for iterators that switch between multiple modes (surely other kinds of modal things too). You don't want it to be constructable by a user since you might change it later, but you do want to use it in a public return value. Maybe the enum could be marked with an attribute that makes all of its sub-variants private/unconstructable or something?

EDIT: Actually, I suppose you can do this already by stuffing a public enum into a private module. It's not intuitive, but it does achieve the function of making the enum inaccessible, yet still usable as an associated type / return type. Or maybe not, since you can still construct it through the associated type.

I don't think individually private variants are a good idea, but I like the sound of all variants being private at once.

Your friend, Yokin

Thats a neat use case! Like I said above, unconstructable (to the end user) variants makes sense, but making them entirely private defeats the pourpose. AN attribute could defintely be a good way to do it. Though like you mentioned here, if an enum is public access but cannot be constructed manually that makes sense to have all-private variants, even if its basically the same as all-uncontructable variants. You may, for example, want to match against what type of iterator it is.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants