Illogical: Cannot use mod in one file but can in another

How is it possible that having the structure of files as shown in the pictures attached, I can successfully use in main.rs:

mod kitchen;

but trying to do exactly the same in thread_pool.rs I'm getting errors!!!

main.rs is special in that it looks in the current directory for modules. Everything else will look in a subdirectory, so mod kitchen; in thread_pool.rs tries to load the file thread_pool/kitchen.rs.

A mod statement defines a new module, so it should appear only once in your project for each module. If you want to refer to it somewhere else, you should write a use statement instead.


As an aside, one of the biggest factors for success in learning Rust (or anything else) is keeping an open mind and trying to understand why things work the way they do instead of expecting them to conform to your expectations. If something seems illogical on the surface, that’s a good sign the real situation is more nuanced than you originally expected.

5 Likes

OK, I'm trying to have an open mind, but how can I understand this?
There are two files in the same directory. In one of them the statement work and in the other, exactly the same statement doesn't? I'm sorry but this is illogical.

Btw, if I write in the thread_pool.rs I'm getting error :

use kitchen;
^^^^^^^ no `kitchen` external crate

You need this to import it.

use crate::kitchen;

(assuming there is a mod kitchen in main.rs)

2 Likes

Thanks, but to be honest, this is bizarre.
What does mod kitchen; do? Do I have to declare it in main? If so why?

The module rules are somewhat different from other languages, but it all makes sense once you understand the rules. A mod statement tells the compiler about a new file it has not seen before. A use statement lets you use things that already have a mod statement somewhere.

There are a few rules for how the compiler finds the file when it sees a mod filename statement:

Every directory has a unique root file. For the src/ directory this is main.rs or lib.rs depending on whether it's an application or library. Whenever the compiler sees a mod myfile; declaration in a file named e.g. parent.rs, it will try to find the file in these two places:

  1. As myfile.rs in the directory that parent.rs is the root file for.
  2. As myfile/mod.rs in the directory that parent.rs is the root file for.

In either case, the file it finds above will become the root-file for the myfile directory (if it exists). Only one of the two files above may exist.


In your example, main.rs is the root-file of src/. The kitchen.rs file would be the root-file of src/kitchen/ if it exists, and thread_pool.rs would be the root-file of src/thread_pool/, if the directory exists.

To tell the compiler about a file, place a mod filename; statement in the root-file for the directory it is in.

8 Likes

Thanks, but really... I've never come across such convoluted/nonintuitive way of importing files.

Generally the complexity comes from the fact that it is possible to not include every file in the build, which in some cases is useful for conditional compilation, e.g. "use this file on windows, and this file on mac".

The use statements act completely like imports from other languages. The weird one is mod, which tell the compiler about new files.

8 Likes

Rust is certainly not afraid of doing things differently than other languages. Whether or not those are intuitive depends more on the background of the learner than anything else.

5 Likes

This is nothing new. If I don't want to import file in a build I simply will not put the import statement. Really. I cannot understand this way. Why wouldn't use statement be enough? I mean...

Say, you have foo_windows.rs and foo_unix.rs, which provides same interface but implementations are platform dependant. In Rust we put conditional inclusion logic only on the mod statement and other modules simply refer it as use crate::foo. If we don't have mod but modules still independently use it, we should put conditionals into every use-es.

1 Like

Would you mind providing an example. It does sound interesting but normally such platform dependent solutions are dealt with in other languages too and it just works without causing confusion like in rust.

Here's an example:

// src/lib.rs
#[cfg(windows)]
#[path = "foo_windows.rs"]
mod foo;

#[cfg(not(windows))]
#[path = "foo_unix.rs"]
mod foo;

mod bar;
// src/foo_windows.rs
fn my_func() {
    println!("I'm on windows");
}
// src/foo_unix.rs
fn my_func() {
    println!("I'm not on windows");
}
// src/bar.rs
use crate::foo::my_func;

fn test() {
    my_func();
}

You may like this article.

5 Likes

It also makes for better diagnostics. If you have a feature dependent module like

#[cfg(feature = "foo")]
mod foo;

then a stray use crate::foo will result in an immediate and obvious error. If the feature dependency were to be on the import, rustc would try to load the file in and in the best case fail at compile time with whatever inside the module isn't supported.

A worse cases would be some compile-time dependency that is satisfied on the developers machine, meaning it will actually build fine, while someone else trying to compile it will get build errors because their system lacks some library.

An even worse case would be that it compiles for the user but only fails at runtime or does something wrong.

Some of these failure cases, especially the latter ones, could even possibly make it through CI.

So I find it makes sense to put these considerations at the point where a module is introduced into the program, and then have the language guard the uses of the built up module tree.

2 Likes

Another thing that mod statements are used for is deciding whether a module is public or not.

mod module_name;
// vs
pub mod module_name;

You can't really make this decision on the import.

4 Likes

I sympathize. The means my which one makes use of code in other files in Rust is as confusing as hell and I have spent hours trying to split a big file of code into some manageable smaller ones. Typically resolved by asking here.

I'm told it is very simple and logical if one knows the rules. But I have never seen those rules rigorously defined or spelled out anywhere.

It did not help me much that the compiler, on not finding something, gives contrary advice as to what to do about it.

It does not help that many crates out there suggest using some pre-2018 Rust approcah to this.

Still, over the years I have seen people struggling with includes and name spaces in C++. I imagine it gets worse when C++ gets modules soon.

Anyway, where is the short an sweet, definitive guide as to how to do this simple thing?

1 Like

This almost exactly how it's how it's done in Python. You'll see a lot of similarity between mod.rs and __init__.py.

The idea is cargo can give the compiler the name of the root file (e.g. main.rs or lib.rs) and from there the compiler will discover how the module tree is laid out by looking for mod declarations. It then uses the rules @alice mentioned to find the source file mod foo is referring to (either ./foo.rs or ./foo/mod.rs).

That's it.

Things like re-exporting are just an artifact of the way pub will make an item (function, type, or even another module) accessible if you use :: to name things inside the module.

1 Like

To make such a simple task so confusing and convoluted... I never struggled in any other language to import a file!!! I mean, it is not like I'm trying to do something unusual...
I don't know what to do about it... If such basic tasks requires such...

I found this blog post which does seems to explain it well.

Interestingly the post lacks the why the module system doing so, which is explained better on this thread.

1 Like
2 Likes