Revisiting modules, take 3

I don't think "path confusion" is as simple as it is made out to be. That is, I think the underlying problem is not that import statements take an absolute path, and local statements take a relative path. In the previous thread, @theduke listed several minstream languages which use absolute paths for their import statements. I would add Ruby to this as well, though its UX is a bit worse because its require-based.

Our problem is a bit different, and I think its got more to do with the shift from a single-file project to a multi-file project. I think in a single-file project, you get bad hints (from the everything is an item situation) that lead you down the wrong road.

For example, you might think that once you've added a dependency, you can use it everywhere. But the reason you can use it your main.rs file is that extern crate foo; is an item, which adds "foo" to the namespace of the root. That's not an obvious fact to many users. Solution: we make all imports from external crates act the same way.

Another problem is that you write code like this once you add a submodule to your project:

mod foo;
use foo::Bar;

Now you have both foo and Bar in scope. You might think that because you declared mod foo;, you can now use from foo. But this is only true in the root module, in other modules you have to say use self::foo::.

A way to mitigate this is to only have imports and not mod statements. If you don't say mod foo, maybe you don't build a mental model that you'll be able to say use foo because you have that special "mod import" that you have to do when you add files.

I don't think this problem is entirely mitigated, and I would like for use foo, where foo is a submodule, to work in every module. I've had trouble finding a solution though, because of the problems that fallback creates with re-exports that pcwalton mentioned in the earlier thread.


However, I think path confusion is also mitigated to some extent by the from syntax. That is, lets say your imports look like this:

from std use collections::HashMap;
from std use fmt;
from std use path::{PathBuf, Path};

from semver use Version;
from serde  use ser;

use core::{Dependency, PackageId, Summary, SourceId, PackageIdSpec};
use core::WorkspaceConfig;

(These are the imports in cargo's core/manifest.rs module.)

My belief is that by making from <crate> a separate syntactic unit, users are hinted that the use core statements have had a from component elided. This makes it easier to internalize that bare use statements have the from crate statement elided on them.

Similarly, I think that the from syntax also makes it easier to understand that use and export have different elided prefixes. "use = from crate, export = from self."

5 Likes