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

Extensible enums #757

Closed
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
140 changes: 140 additions & 0 deletions text/0000-extensible-enums-and-structs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
- Start Date: (fill me in with today's date, YYYY-MM-DD)
- RFC PR: (leave this empty)
- Rust Issue: (leave this empty)

# Summary

Introduce a notation for declaring an enum to be *extensible*, meaning
that more variants or fields may be added in the future. This notation
does not affect the use of the enum within the current crate, but it
prevents downstream crates from employing features (such as exhaustive
matches or struct literals) that would fail to compile if new
variants/fields were added.

# Motivation

Imagine that one is writing a public-facing API that includes an
`Error` enum (this exact scenario arises in libstd with the I/O error
enum):

```rust
pub enum Error {
NoSuchFile,
NoPermissions,
}
```

Providing an enum listing out the possible sources of error is a very
convenient API for your users, because they can be use a `match`
statement to easily identify and handle particular kinds of
errors. Unfortunately, it's rather limiting on the future evolution of
your library: if you wish to add a third kind of error in the future,
you will be potentially breaking downstream code. After all, it is
possible that some user was doing an exhaustive match against all
possible sources of error:

```rust
match error {
Error::NoSuchFile => ...,
Error::NoPermissions => ...,
}
```

To resolve this dilemna, this RFC proposes permitting enums to be declared
as *extensible*:

```rust
pub enum Error {
NoSuchFile,
NoPermissions,
..
}
```

Extensible enums from other crates can never be exhaustively
matched. This means that a downstream user attempting to match against
an error from your library would have to include a wildcard option:

```rust
// from an external crate
match error {
Error::NoSuchFile => ...,
Error::NoPermissions => ...,
_ => ...,
}
```

Due to the presence of this wildcard, you can safely add variants
without fear of breaking source compatibility with downstream clients.

Note that extensibility is ignored within the current
crate. Therefore, your library may internally write a match that
exhaustively covers all sources of error. You don't need to worry
about source compatibility with yourself, after all.

# Detailed design

Enum grammar is extended to permit a list of variants to be terminated
with `..`. An enum declared with `..` is considered *extensible*.
Copy link
Member

Choose a reason for hiding this comment

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

Excuse the bikeshedding, but I'd like to propose ... rather than .. because a) .. is much more heavily overloaded in Rust already than ... and b) ... actually means 'elided things' in English writing.

Copy link
Member

Choose a reason for hiding this comment

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

I guess .. is consistent with elided fields in struct patterns (fwiw, I don't like that either, especially now we have .. for ranges).

Copy link
Contributor

Choose a reason for hiding this comment

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

+1 for ...
On Jan 29, 2015 7:50 AM, "Nick Cameron" [email protected] wrote:

In text/0000-extensible-enums-and-structs.md
#757 (comment):

  • _ => ...,
    +}
    +```

+Due to the presence of this wildcard, you can safely add variants
+without fear of breaking source compatibility with downstream clients.
+
+Note that extensibility is ignored within the current
+crate. Therefore, your library may internally write a match that
+exhaustively covers all sources of error. You don't need to worry
+about source compatibility with yourself, after all.
+
+# Detailed design
+
+Enum grammar is extended to permit a list of variants to be terminated
+with... An enum declared with.. is considered extensible.

Excuse the bikeshedding, but I'd like to propose ... rather than ..
because a) .. is much more heavily overloaded in Rust already than ...
and b) ... actually means 'elided things' in English writing.


Reply to this email directly or view it on GitHub
https://github.com/rust-lang/rfcs/pull/757/files#r23735568.


Extensible enums that are local to the current crate are treated the
same as any other enum. Exhaustiveness checking is modified so that
extensible enums that are not local to the current crate are never
considered completely covered. (As if there was one additional variant
beyond those declared, essentially.)

Extensibility does not currently affect the enum in any other way. The
representation in particular remains unchanged. This RFC does not
attempt to address binary-level compatibility, only source
compatibility.

# Drawbacks

More language syntax.

# Alternatives

Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure it is better, but an alternative design would be for matches in the same crate to also require a wildcard match arm. I realise this is not necessary, but it would make things be more consistent.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, true.

Choose a reason for hiding this comment

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

Edit: I saw the comment below only now, sorry. Consider my question answered.
I also buy the consistency argument: in particular since moving modules between crates should really not break pattern matching expressions. Maybe place that relaxation on modules rather than crates?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

On Tue, Feb 03, 2015 at 02:39:32AM -0800, Cristi Cobzarenco wrote:

I also buy the consistency argument: in particular since moving modules between crates should really not break pattern matching expressions. Maybe place that relaxation on modules rather than crates?

I'm not sure why you would think this. There are already things that
are affected by moving between crates: impls come to mind.

**Private enum variants.** There are various privacy-based workarounds
one could use to get a similar guarantee. For example, if private
variants were permitted (or perhaps variants were private by default),
then one could add a private variant to the enum. Privacy however is not a perfect fit:

1. Private variants would also be private within your crate,
preventing you from using local exhaustive matching as well.
2. Even if you work around the previous problem and manage to make
enum variants visible only within the current crate, all of your
match statements will include an extra, unreachable arm
corresponding to this private variant (since it never occurs in
practice).

**Extensible structs.** In the same way that enums can be extended
with more variants, it might be useful to be able to declare structs
as being extensible with additional fields. This would prevent the
various bits of syntax (struct literal matching, struct literal
constructors) that require knowledge of the full set of fields to
work. Originally this feature was intended for inclusion in this RFC
but was omitted to keep things simple. The need to add more fields in
the future is both rather unusual and easily addressed using a dummy
private field (private fields do have some of the same downsides as
private enum variants, though). In practice though most libraries will
just declare all fields as private and use getters to access their
contents, which is a better pattern in any case.

**Treat extensible enums as extensible also within the local crate.**
The current design states that extensible enums are only considered
"extensible" outside the current crate. The intention was to allow for
more precision locally, so that users can still take advantage of
exhaustive matches. However, this introduces an asymmetry between
crates; further, in many real-world use cases (e.g., `IoError`), it is
unlikely that even the local crate will ever want to exhaustively
match all variants, so this extra expressive power may not be
necessary.

# Unresolved questions

Is there a way to have downstream crates get warnings -- if not errors
-- when a match is not exhaustive? One suggestion was to add an
alternate wildcard syntax that indicates a match that is intended to
be exhaustive, even if a backup must be provided. But it is unclear
what that wildcard syntax should be (the suggestion was `..`, but that
is ambiguous with existing syntax).