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

Define variants in a sub-namespace of their enum #390

Merged
merged 2 commits into from
Oct 31, 2014
Merged
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
333 changes: 333 additions & 0 deletions active/0000-enum-namespacing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,333 @@
- Start Date: 2014-07-16
- RFC PR #:
- Rust Issue #:

# Summary

The variants of an enum are currently defined in the same namespace as the enum
itself. This RFC proposes to define variants under the enum's namespace.

## Note

In the rest of this RFC, *flat enums* will be used to refer to the current enum
behavior, and *namespaced enums* will be used to refer to the proposed enum
behavior.

# Motivation

Simply put, flat enums are the wrong behavior. They're inconsistent with the
rest of the language and harder to work with.

## Practicality

Some people prefer flat enums while others prefer namespaced enums. It is
trivial to emulate flat enums with namespaced enums:
```rust
pub use MyEnum::*;

pub enum MyEnum {
Foo,
Bar,
}
```
On the other hand, it is *impossible* to emulate namespaced enums with the
current enum system. It would have to look something like this:
```rust
pub enum MyEnum {
Foo,
Bar,
}

pub mod MyEnum {
pub use super::{Foo, Bar};
}
```
However, it is now forbidden to have a type and module with the same name in
the same namespace. This workaround was one of the rationales for the rejection
of the `enum mod` proposal previously.

Many of the variants in Rust code today are *already* effectively namespaced,
by manual name mangling. As an extreme example, consider the enums in
`syntax::ast`:
```rust
pub enum Item_ {
ItemStatic(...),
ItemFn(...),
ItemMod(...),
ItemForeignMod(...),
...
}

pub enum Expr_ {
ExprBox(...),
ExprVec(...),
ExprCall(...),
...
}

...
```
These long names are unavoidable as all variants of the 47 enums in the `ast`
module are forced into the same namespace.

Going without name mangling is a risky move. Sometimes variants have to be
inconsistently mangled, as in the case of `IoErrorKind`. All variants are
un-mangled (e.g, `EndOfFile` or `ConnectionRefused`) except for one,
`OtherIoError`. Presumably, `Other` would be too confusing in isolation. One
also runs the risk of running into collisions as the library grows.

## Consistency

Flat enums are inconsistent with the rest of the language. Consider the set of
items. Some don't have their own names, such as `extern {}` blocks, so items
declared inside of them have no place to go but the enclosing namespace. Some
items do not declare any "sub-names", like `struct` definitions or statics.
Consider all other items, and how sub-names are accessed:
```rust
mod foo {
fn bar() {}
}

foo::bar()
```

```rust
trait Foo {
type T;

fn bar();
}

Foo::T
Foo::bar()
```

```rust
impl Foo {
fn bar() {}
fn baz(&self) {}
}

Foo::bar()
Foo::baz(a_foo) // with UFCS
```

```rust
enum Foo {
Bar,
}

Bar // ??
```

Enums are the odd one out.

Current Rustdoc output reflects this inconsistency. Pages in Rustdoc map to
namespaces. The documentation page for a module contains all names defined
in its namespace - structs, typedefs, free functions, reexports, statics,
enums, but *not* variants. Those are placed on the enum's own page, next to
the enum's inherent methods which *are* placed in the enum's namespace. In
addition, search results incorrectly display a path for variant results that
contains the enum itself, such as `std::option::Option::None`. These issues
can of course be fixed, but that will require adding more special cases to work
around the inconsistent behavior of enums.

## Usability

This inconsistency makes it harder to work with enums compared to other items.

There are two competing forces affecting the design of libraries. On one hand,
the author wants to limit the size of individual files by breaking the crate
up into multiple modules. On the other hand, the author does not necessarily
want to expose that module structure to consumers of the library, as overly
deep namespace hierarchies are hard to work with. A common solution is to use
private modules with public reexports:
```rust
// lib.rs
pub use inner_stuff::{MyType, MyTrait};

mod inner_stuff;

// a lot of code
```
```rust
// inner_stuff.rs
pub struct MyType { ... }

pub trait MyTrait { ... }

// a lot of code
```
This strategy does not work for flat enums in general. It is not all that
uncommon for an enum to have *many* variants - for example, take
[`rust-postgres`'s `SqlState`
enum](http://www.rust-ci.org/sfackler/rust-postgres/doc/postgres/error/enum.PostgresSqlState.html),
which contains 232 variants. It would be ridiculous to `pub use` all of them!
With namespaced enums, this kind of reexport becomes a simple `pub use` of the
enum itself.

Sometimes a developer wants to use many variants of an enum in an "unqualified"
manner, without qualification by the containing module (with flat enums) or
enum (with namespaced enums). This is especially common for private, internal
enums within a crate. With flat enums, this is trivial within the module in
which the enum is defined, but very painful anywhere else, as it requires each
variant to be `use`d individually, which can get *extremely* verbose. For
example, take this [from
`rust-postgres`](https://github.com/sfackler/rust-postgres/blob/557a159a8a4a8e33333b06ad2722b1322e95566c/src/lib.rs#L97-L136):
```rust
use message::{AuthenticationCleartextPassword,
AuthenticationGSS,
AuthenticationKerberosV5,
AuthenticationMD5Password,
AuthenticationOk,
AuthenticationSCMCredential,
AuthenticationSSPI,
BackendKeyData,
BackendMessage,
BindComplete,
CommandComplete,
CopyInResponse,
DataRow,
EmptyQueryResponse,
ErrorResponse,
NoData,
NoticeResponse,
NotificationResponse,
ParameterDescription,
ParameterStatus,
ParseComplete,
PortalSuspended,
ReadyForQuery,
RowDescription,
RowDescriptionEntry};
use message::{Bind,
CancelRequest,
Close,
CopyData,
CopyDone,
CopyFail,
Describe,
Execute,
FrontendMessage,
Parse,
PasswordMessage,
Query,
StartupMessage,
Sync,
Terminate};
use message::{WriteMessage, ReadMessage};
```
A glob import can't be used because it would pull in other, unwanted names from
the `message` module. With namespaced enums, this becomes far simpler:
```rust
use messages::BackendMessage::*;
use messages::FrontendMessage::*;
use messages::{FrontendMessage, BackendMessage, WriteMessage, ReadMessage};
```
Copy link
Contributor

Choose a reason for hiding this comment

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

Have glob imports been accepted for 1.0 yet? Because if not, then there's no benefit to a namespace here, as you'd have to import them all manually anyway.

Copy link
Member Author

Choose a reason for hiding this comment

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

@pcwalton do you know what the story is with the future of #[feature(globs)]?

The last I heard is that pub-use globs are the real troublemakers, but that the problems seem fixable.

Copy link
Contributor

Choose a reason for hiding this comment

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

@nikomatsakis's name resolution prototype includes support for globs. (From this I would assume that they will be supported, but don't take it from me as official.)


# Detailed design

The compiler's resolve stage will be altered to place the value and type
definitions for variants in their enum's module, just as methods of inherent
impls are. Variants will be handled differently than those methods are,
however. Methods cannot currently be directly imported via `use`, while
variants will be. The determination of importability currently happens at the
module level. This logic will be adjusted to move that determination to the
definition level. Specifically, each definition will track its "importability",
just as it currently tracks its "publicness". All definitions will be
importable except for methods in implementations and trait declarations.

The implementation will happen in two stages. In the first stage, resolve will
be altered as described above. However, variants will be defined in *both* the
flat namespace and nested namespace. This is necessary t keep the compiler
bootstrapping.

After a new stage 0 snapshot, the standard library will be ported and resolve
will be updated to remove variant definitions in the flat namespace. This will
happen as one atomic PR to keep the implementation phase as compressed as
possible. In addition, if unforseen problems arise during this set of work, we
can roll back the initial commit and put the change off until after 1.0, with
only a small pre-1.0 change required. This initial conversion will focus on
making the minimal set of changes required to port the compiler and standard
libraries by reexporting variants in the old location. Later work can alter
the APIs to take advantage of the new definition locations.

## Library changes

Library authors can use reexports to take advantage of enum namespacing without
causing too much downstream breakage:
```rust
pub enum Item {
ItemStruct(Foo),
ItemStatic(Bar),
}
```
can be transformed to
```rust
pub use Item::Struct as ItemStruct;
pub use Item::Static as ItemStatic;

pub enum Item {
Struct(Foo),
Static(Bar),
}
```
To simply keep existing code compiling, a glob reexport will suffice:
```rust
pub use Item::*;

pub enum Item {
ItemStruct(Foo),
ItemStatic(Bar),
}
```
Once RFC #385 is implemented, it will be possible to write a syntax extension
that will automatically generate the reexport:
```rust
#[flatten]
pub enum Item {
ItemStruct(Foo),
ItemStatic(Bar),
}
```

# Drawbacks

The transition period will cause enormous breakage in downstream code. It is
also a fairly large change to make to resolve, which is already a bit fragile.

# Alternatives

We can implement enum namespacing after 1.0 by adding a "fallback" case to
resolve, where variants can be referenced from their "flat" definition location
if no other definition would conflict in that namespace. In the grand scheme of
hacks to preserve backwards compatibility, this is not that bad, but still
decidedly worse than not having to worry about fallback at all.

Earlier iterations of namespaced enum proposals suggested preserving flat enums
and adding `enum mod` syntax for namespaced enums. However, variant namespacing
isn't a large enough enough difference for the additon of a second way to
define enums to hold its own weight as a language feature. In addition, it
would simply cause confusion, as library authors need to decide which one they
want to use, and library consumers need to double check which place they can
import variants from.

# Unresolved questions

A recent change placed enum variants in the type as well as the value namespace
to allow for future language expansion. This broke some code that looked like
this:
```rust
pub enum MyEnum {
Foo(Foo),
Bar(Bar),
}

pub struct Foo { ... }
pub struct Bar { ... }
```
Is it possible to make such a declaration legal in a world with namespaced
enums? The variants `Foo` and `Bar` would no longer clash with the structs
`Foo` and `Bar`, from the perspective of a consumer of this API, but the
variant declarations `Foo(Foo)` and `Bar(Bar)` are ambiguous, since the `Foo`
and `Bar` structs will be in scope inside of the `MyEnum` declaration.