rfcs
rfcs copied to clipboard
Support Private Enum Variants
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: https://github.com/rust-lang/rfcs/pull/2028 https://github.com/rust-lang/rust/issues/32770
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() }
I feel that privacy goes contrary to the main advantages of enums:
-
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.
-
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 typesyn::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. -
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.
-
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
andBar
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
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
@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.
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.
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
I'm even skeptical of the
#[non_exhaustive]
attribute. Like, if I'm trying to exhaust a value of typesyn::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.
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.
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 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
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.
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.
That's just marking with the variant with #[non_exhaustive]
, which makes the variant constructor pub(crate)
.
@Yokinman for the iterator case, isn't the usual approach to wrap the enum in a struct such that the enum doesn't need to be pub
?
Here's a usecase: I have an enum
enum Foo {
Bar,
Baz,
Qux
}
And I want to run code when, say, Foo::Bar
is instantiated.
One case I frequently run into is mapping non-exhausitive enums from an external system (e.g. a JSON or C API) which represents enums as string or integer. I'd like to preserve unknown values, while acting as similar as possible to a normal non-exhaustive enum in rust.
So I'd like to write something like:
pub enum MyEnum {
Value1,
Value2,
#[non_exhaustive]
private Unknown(String),
}
From outside the crate this enum would behave like:
#[non_exhaustive]
pub enum MyEnum {
Value1,
Value2,
}
Outside the crate the Unknown
variant would be inaccessible, which has two important benefits:
- An outsider won't be able to put a known value into
Unknown
(MyEnum::Unknown("Value1")
would produce and invalid value) - An outsider won't be able to match on
Unknown
, so adding a new known value is not a breaking change
But traits like FromStr
and Display
could still access the Unknown
variant, making conversion from/to strings infallible and lossless.
Currently it's necessary to resort to ugly workarounds, like putting the enum inside wrapper.