Skip to content

Instantly share code, notes, and snippets.

@withoutboats
Created January 3, 2017 03:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save withoutboats/1c7c74236bc4754b2f0ea1e13e06e081 to your computer and use it in GitHub Desktop.
Save withoutboats/1c7c74236bc4754b2f0ea1e13e06e081 to your computer and use it in GitHub Desktop.

This document is the result of conversations that came out of this tweet: https://twitter.com/withoutboats/status/814201265575981056

Rust's module system is too confusing

Empirically, very many new users of Rust are confused by Rust's module system. This is unfortunate: Rust's module system is not particularly innovative or powerful; it is intended only to provide fairly standard privacy and namespacing support. Too much of new users' attention is being pulled by this system as it exists today.

This document presents an hypothesis of the cause of the confusion, and an attempt to mitigate that confusion by instituting a practice that is more similar to mainstream languages. We believe this problem is caused by the confluence of a several well-motivated design decisions that have created a very unusual system, and the solution is to require less declarations by leveraging ambient information in a manner more similar to how other languages' module systems work.

Rust requires users to build an explicit module graph

Every language which supports some namespacing necessarily defines a graph of namespaces which are traversed as names are resolved to their canonical origin. Rust takes the uncommon approach of having users explicitly construct the canonical module tree with mod declarations before allowing them to create additional 'facade' paths with use and pub use.

It is more common for languages to implicitly take the canonical tree from the file system hierarchy, and only allow users to import names from other points in the tree into the local namespace with a statement equivalent to use.

Rust allows users to make much more complex statements about the module graph than many other languages. By default, canonical paths are private outside of their immediate module, and must be made private. It also allows creating totally artificial paths to names with re-exports. Any change should preserve this ability.

Some Rust source files are branch nodes in the module graph

In languages with an implicit canonical path tree, it is not uncommon to treat each file as a 'leaf' node of the graph; no other namespaces are within the namespace of any file without explicitly importing them. However, Rust does treat submodules as inherently in scope to their parent module.

Combined with the requirement to explicitly declare modules, this causes a mod declaration like mod foo; to behave similarly to how import foo; would work in many languages. This makes it very difficult for many users to understand what mod even does that is different from use, and it can feel arbitrary when mod is appropriate and when use is.

Rust simply has too much syntax for modules

Rust has essentially four syntactic forms for dealing with the module graph:

  1. mod for declaring a submodule.
  2. extern crate for declaring an external node of the module graph.
  3. use for bringing names into the namespace of the local module.
  4. pub use for creating facade paths to names in the graph.

The distinction between these terms are often subtle and difficult for new users to grasp. It would be advantageous for Rust to simply have less syntax.

Steps to mitigate

Infer mod graph from source directory tree

Even after you understand Rust's module system, the requirement to make mod declarations can feel quite redundant, since the location of the file will be inferred from the filesystem. It makes a lot of sense to allow users to skip this step, which feels like bookkeeping at best and a totemic 'explicitness token' at worst.

When building the canonical paths, walk the entire directory

Instead of only following explicit mod directives while building canonical paths, instead the tree will be constructed from every file in the source directory that matches the Rust moudle tree naming scheme that is already defined. Every file found will be added to canonical module tree as if it had been declared with mod foo. All modules are parsed and walked by macros the same as ever (e.g. by the test harness).

However, if there is no explicit mod foo declaration, the foo module will be treated as an "implicit submodule," not an "explicit submodule."

Treat implicit submodules as not in their parent's namespace

Implicit submodules are not implicitly present in their parent's namespace. This way, there are no names that are in the parent module which are not explicitly declared.

Instead, if a user wishes to access names from implicit submodules, or make them public, they should use a use declaration:

// Instead of:
// mod foo;
// pub mod bar;

use self::foo;
pub use self::bar;

Discourage the use of bodiless mod.

Having done this, bodiless mod declarations will have no purpose. Attributes on modules can be attached at the top of the module file with the #! form.

Inline modules with bodies (as are commonly used for tests modules fo rexample) will continue to exist, and mod will continue to be used for that purpose.

Make extern crate unnecessary with cargo

When adding external dependencies, users using cargo need to declare them in multiple locations - in their Cargo.toml file and in the root of their binary. This is redundant work. Once again, this is either bookkeeping or an 'explicitness token.'

rustc infers extern crate from --extern flags

Instead, when rustc receives an argument of the form --extern NAME=PATH, NAME will implicitly be attached at the canonical root of the module graph, directed to that path. This name has the lowest precedence; it will be shadowed by any declared name in the root of the graph (including by an explicit extern crate declaration).

Cargo supports alias attribute on dependency

Add an alias attribute to the dependency object in a Cargo.toml file. This attribute takes a string, and instead of passing the crate (with the --extern flag) by its actual name, cargo will present the crate to rustc using this alias. This makes extern crate .. as ..; unnecessary.

Result

The result of these changes is that mod and extern crate declarations would become unnecessary, leaving only use and its cousin pub use as namespace management syntax. The only exception to this would be inline submodules, for which the mod keyword is much more intuitive than with multiple files.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment