Sibling modules and traits

Trying to make sure I understand this ... finally. Here is a working example where I don't understand why some lines are needed.

Here is src/printable.rs:

pub trait Printable {
    fn print(&self);
}

Here is src/person.rs:

// Why do I not need "mod printable;" here?

// Why do I need "crate::" on the next line, but I don't need it in
// the "use printable::Printable" that appears in src/main.js below?
use crate::printable::Printable;

pub struct Person {
    pub first_name: String,
    pub last_name: String,
}

impl Printable for Person {
    fn print(&self) {
        println!("{} {}", self.first_name, self.last_name);
    }
}

Here is src/main.rs:

mod person;
use person::Person;

// I think I need this in order to have the next line,
// but I didn't need this in src/person.rs.
mod printable;

// Why do I need this at all? See my question at the bottom.
use printable::Printable;

fn main() {
    let p = Person {
        first_name: "Mark".to_string(),
        last_name: "Volkmann".to_string(),
    };
    p.print();
}

My biggest question is why a trait needs to be in scope in order to call its methods. It seems like it should be enough to have the Person struct in scope because its definition is what provides the implementation of the print method. Is there a technical reason why the Rust compiler cannot support this?

Rust only goes looking in the filesystem for source files that it’s explicitly requested to. You make that request via the mod printable; statement, which creates a new entry in the module tree when it’s processed by the compiler.

The module tree for this example looks like this:

  • crate (main.rs)
    • crate::printable (printable.rs)
    • crate::person (person.rs)

This is entirely defined by where the corresponding mod statements appear, and not where the source files happen to be: You’d have exactly the same module tree if you included all of the code from person.rs and printable.rs inline in mod name { ... } blocks.

Paths that don’t start with crate::, super::, or :: are relative to the module in which they appear. Because this statement is inside the crate::person module, you need to either traverse up a level with super:: or start from the module tree root with crate:: to access the sibling module. Otherwise, you’d be trying to access the crate::person::printable module, which doesn’t exist. Inside main.rs, you’re inside crate directly so there’s no need for the explicit path, though crate::printable will work there too.

There needs to be exactly one mod printable; statement in your project1. It instructs the compiler to read in printable.rs as a module defined here.

Rust allows other crates and modules to also define traits that have a print() method and implement them for Person. This rule is to prevent code breaking with an ambiguity when you link in some crate that does this: the code knows which print() method you mean because it’s the one in scope.


1 See @arnaudgolfouse‘s comment for a clarification.

4 Likes

A big thing I was missing is that in my example person and printable are siblings, but neither of those is a sibling of main. I had no idea that was the case!

Would you say that in general mod statements should all go in the top file (main.rs or lib.rs)?

Note, the following post does not apply to edition = "2015", but rather, the 2018 edition and onwards

The idea, which has been witnessed to be counter-intuitive when starting, is that the file hierarchy does not exactly match the module hierarchy, especially at the "beginning" of the src/ directory.

I'm gonna try an alternative way of explaining it: imagine your src/main.rs was actually an src.rs file.

It represents the root of the code in your (binary) crate, and can have submodules defines within the src/ directory.

Then, a submodules would "always" a submodule.rs, which can, in and of itself, have nested submodules:

src.rs
src/
  printable.rs
  printable/...

  person.rs
  person/...

In your case:

  • your printable and person submodules do not appear to feature submodules of their own;

  • src.rs does not actually exist, it is, instead, named src/main.rs (for a (directly executable) binary / an application), or named src/lib.rs for a library.

With this, it should be now clear that src/main.rs sits above the other two "files" / modules, which are "siblings" / of the same depth.

Another way of representing this file hierarchy is using the mod.rs naming convention:

src/
  mod.rs (in the case of src, it's actually main.rs or lib.rs)
  printable/
    mod.rs
    some_nested_module/
      ...
  person/
    mod.rs

This view, although a bit more cumbersome, has the advantage of better mapping the Rust paths to the file paths:

  • the root of the paths is src/, which contains the items defined in that "src module" / crate root.
    In rust, such root is written as crate::

    We have, for instance, the items:

    • crate::main, a function, defined in src/mod.rs src/main.rs

    • crate::printable, a module, i.e., something that can contain items inside: crate::printable::some_item. Such items are defined in src/printable/mod.rs (or src/printable.rs for the other file naming convention).

    • crate::person, anoter module.

Now, imagine, within such a file system, that you are inside the /printable/ directory, and that you want to refer to a file within the /person/ directory. If you used a non-qualified path, such as person/... (the equivalent of use person::... in Rust), that would be looking for a person directory (module) nested within your current directory (current module). And you don't have a src/printable/person/... file structure, so that would fail.

You have, on the other hand, two approaches to refer to the contents of that sibling-in-parent-dir directory:

  • either you use the ../person path (in Rust: use super::person::...),

  • or, since in this instance, the parent .. (super) is actually the root of the paths / (crate::), you can use that absolute path:

    /person/... (in Rust: crate::person::...)

And when you are within the "src module" = root-of-the-crate module (src/mod.rs src/main.rs file), you can still use both approaches:

  • either the relative path person/... which leads to use person::... in Rust;

  • or the absolute path /person/... wich would lead, in Rust, to use crate::person::...

Since the former is shorter, we thus usually elide the unnecessary / redundant absolute path specified.


I hope this clarifies the pathing question. It turns out that it's the "ergonomic" shortcuts (naming src/mod.rs as src/main.rs and being allowed to use module_name.rs instead of module_name/mod.rs) which make visualizing the module hierarchy not obvious (e.g., src/{main,printable,person}.rs being in the same directory).


Regarding:

This is because modules are not "auto-included", even if there are .rs within the src/ directory: you have to "acknowledge" their existence by declaring them within the parent module contents, by using that mod modname; line.


This is by design, to avoid "action at a distance" when dealing with a complex program structure and libraries:

  • Imagine you have a colleague working with some type, say, a foo: Foo and calling foo.some_method() on it.

  • Now imagine that you, on your own different module, define a new trait,

    trait SomeTrait {
        fn some_method (&self) ...
    }
    

    and decide that it would be convenient for you that Foos implement your helper trait:

    impl SomeTrait for Foo { ... }
    
  • If the implemented traits weren't required to be in scope, you'd have introduced a method resolution ambiguity, or worse, shadowed an actual method call, within your colleague's code. If your colleague intended to call <Foo as path::to::SomeTrait>::some_method() because they were aware of your code, then they can either use that fully-qualified method call, or they can use path::to::SomeTrait;. Both ways show that your colleague was aware for the existence of SomeTrait / they have explicitly opted into using that part of your code, so there are no suprises this way :slightly_smiling_face:

4 Likes

That could be misleading: you can have

  • In main.rs:
    mod printable;
    fn main() {}
    
  • In printable/mod.rs:
    mod printable;
    
  • And a printable/printable.rs file.

In the same way that the same file name can appear multiple times in the filesystem.

3 Likes

I would say no... Modules are a way to partition the logic of your crate.

If, say, you have a printable module that is supposed to print stuff; and it already contains functions to print text, and floats, and integers... you might want to partition all of that in different files, that each would hold one of the printing function.
But these functions are, overall, all part of the 'printing' stuff, so it could make sense to have something like:

- crate root (main.rs)
  - printable (printable/mod.rs)
    - print_int   (printable/print_int.rs)
    - print_float (printable/print_float.rs)
    - print_str   (printable/print_str.rs)
  - other modules...

Here, main.rs would contain

mod printable;
// other modules ...

and printable/mod.rs

mod print_int;
mod print_float;
mod print_str;
2 Likes