[v3] work out concept(s) for scope, visibility, modules
I see articles every once in a while whose premise is "Rust's module system is confusing; here's how it works." Anecdotally, I feel like we should explicitly teach the module system somewhere. I don't have strong feelings about where.
scope, visibility, and modules are very closely coupled concepts. Perhaps for now we should just try to teach all of them in scope_and_visibility or something, and then just split things out if that concept gets too big.
Originally posted by @coriolinus in https://github.com/exercism/rust/pull/1233#discussion_r612476177
Great idea!
Perhaps for now we should just try to teach all of them in scope_and_visibility or something, and then just split things out if that concept gets too big.
I like this approach. Addressing the core confusion by discussing these cohesively seems like a good first step to me.
Naive learner questions from me:
-
pub(crate)significance and use cases - when to and why care about the visibility of various functionalities
- gloss module resolution via filesystem paths and names. Would be nice to mention Rust ecosystem practices like "Many major projects break up complex functionality into smaller modules represented by single files".
Quick answers:
pub(crate)
This is most useful for larger projects. You'll sometimes want to split your code into several modules. By default, a module's items are visible only to itself and to parent modules. However, sometimes you need to use one module's items in another (private) module within the crate, but you don't want to make it visible to a consumer of your library. That's when pub(crate) comes in handy.
For example:
mod some_private_stuff {
/// A utility struct that various modules in this crate need to see, but shouldn't be visible to the outside world
pub(crate) struct Util;
}
mod other_stuff {
use crate::some_private_stuff::Util;
fn do_something(util: &Util) {}
}
For a real-world example, I made several configuration items visible to the various modules here, but kept that struct opaque to users of the library.
Why do we care?
It's all about managing complexity. Some things just take a lot of implementation. However, if you can provide a simple interface to all that complexity, then your users don't need to care what happens under the hood.
The general rule for a clean-sheet implementation is to make your public interface as small as possible. Make public everything your library needs in order to work, and nothing more. Under the hood, factor your code appropriately; lots of small, simple, single-purpose functions are often the right way to go. Your users don't have to know about any of them.
Module resolution basics
This is covered in depth elsewhere, but the basic rule is that a module's namespace contains:
- everything defined directly in that module
- everything explicitly imported via a
usestatement - crate top-level with
extern crate - public macros with
#[macro_use] extern crate
Navigation
- To navigate the namespaces, use double-colons:
use sub_module::sub_sub_module::FOO;. - To move up one level in the module hierarchy, use the
superkeyword:use super::sibling_module::BAR;. - To go directly to the crate root in the module hierarchy, use the
cratekeyword:use crate::top_level_module::BAT;. - To use an external crate's items, always start from the root using the crate's name:
use num_runtime_fmt::NumFmt;.
After several failed attempts to create a high-quality syllabus, I believe any future attempts will have to do their own design work from scratch.