Help with the new "Packages, crates and modules" chapter of the book?

https://github.com/rust-lang/book/pull/117

This part of the book is extremely important; the module system is something
that trips up new rustaceans a lot. I'm trying to re-work it for the
new book, but am not that psyched about what I've done. I think one of
the reasons is that the example is extremely boring, but I've had a
tough time of coming up with something that's simple, but also not
trivial.

What struggles have you had with the module system? What can I do to help here.

1 Like

The two things that I struggled with the most (and look to be absent) were super, and how it interacts with privacy as well as self, and the differences between use foo, use ::foo, and use self::foo

11 Likes

Why both modules and crates are in the single chapter? I think that the topics are pretty orthogonal and difficult, so perhaps you can have two chapters? One about modules, paths, privacy and directory structure. Another one about crates, libraries, crates.io and perhaps linkers? There is little point in discussing crates if you don't pull any dependencies imo, and you can go a long way with just a single crate.

2 Likes

I personally remember the following revelations from learning about modules system:

  • Paths in use declarations are implicitly "global".
  • You don't need to put extern crate rand into every module which uses rand. Instead you put it only in the lib.rs
  • Corollary of the first two bullets: you use std:: in use and ::std:: in other places.
  • A crate is a tree of modules, and you can specify all modules inline.
  • Modules are for logical separation, and crates are for physical (link time) separation.
  • Modules can have cyclic dependencies, but crates form a DAG (fun fact: packages can form cycles)
  • There is no shared namespace of crates, so linking several different versions of the same crate surprisingly just works.
6 Likes

Especially the first point has confused me a lot.
What's actually the reason for this?

The suggestion to handle modules and crates separately is a good idea. You might also be able to separate out visibility rules as well though I'm not sure. I'm not sure if having to point out the visibility changes every time you make a module change is a good thing. It kinda hides the rules in a wall of text.

It might also be better to start with modules and build up to crates (not sure how this suggestion would interact with the previous comment). Then, you can just say a crate is a special case of a module with these features:

  • A
  • B
  • C

As it is, you're special casing a crate based on something the reader might not understand yet (modules yet to be discovered). That's just asking the reader to backtrack to remember what the original definition of a crate is when it is mentioned again. Might be hard to remember.

Actually, as it is it isn't very clear on the distinction between modules, crates, and packages from the description. A crate is a special case of a module that only occurs at the top level. A package is then at the top level where it encloses multiple crates. These concepts seem similar and the distinction between them might not be clear.

Also, if you ignored modules from external files for the time being and then covered visibility and private/public separately, you could probably rely on the previously defined explanations when pointing out briefly how modules can be externalized almost as a shorthand.

(I hope that made sense...)

Re-exports, especially selective re-exports to a goal consumer facing API surface tripped me up for a long time.

For examples, maybe think about pulling in something from std::io or std::net that split across a sys module. Using an actual stdlib example might help give some insight into design patterns of the stdlib, which could be helpful for navigating the source tree for your first few times.

I agree with the people suggesting treating crates and modules separately. I would discuss modules first.

Towards the end of the chapter you mention:

A package can contain ... as many binary crates as you'd like.

I imagine that's done via src/bin? It would be nice if you gave an example of that too.

How does pub actually work? How does (pub) mod interact with pub struct|fn|enum? Common use case: how do you make something that you can use throughout your crate but is not exported to users of your crate?

Difference between putting extern crate in your crate root and another module.

The text's explanation is that mod foo; expands to mod foo { /* content of foo.rs */ }. This is of course correct, but I found this explanation not very useful for teaching, because no other languages work that way.

One useful way I found is that mod in Rust is declarative syntax for information usually maintained outside the language in other languages. For example, such information is often maintained in IDE project file or build file (Makefile, etc).

This suggests a less boring example: conditional compilation. An example from std is great:

#[cfg(unix)]
#[path = "sys/unix/mod.rs"]
mod sys;

#[cfg(windows)]
#[path = "sys/windows/mod.rs"]
mod sys;

Here's another:

#[cfg(target_os = "linux")]
pub mod linux;

#[cfg(target_os = "macos")]
pub mod macos;

I was incredibly frustrated and dumbfounded that code in lib.rs is special, and can't be copy&pasted unchanged to other files.

That's because paths in use and the rest of the code look the same, but are interpreted differently. Paths like std::fs::File work in one file, and std would not be found when I tried to move the function to anther file. I haven't seen tutorials explicitly warning about this.

All tutorials assume you write code in lib.rs or main.rs, which in a non-trivial project is an exception rather than the rule.

So please, from the start, show how examples work with minimum two files, because it matters where extern crate is put, and matters how std vs ::std works.

2 Likes

I don't get it, use ::std::... and use std::... ought to be equivalent.

That's what caused me pain until someone explained to me that module references in use and actual code are not the same thing, despite looking the same:

fn foo() {
    std::fs::File::open("test"); // OK
}

mod foo {
    fn foo() {
        // ERROR Use of undeclared type or module `std::fs::File`
        std::fs::File::open("test"); 
    }
}

Right, but this is also why this is tricky: with use, they're the same. But if you're just referring to the bare name, it's not gonna work.