Import system for the next language

Disclaimer: this is not a proposal to change, rust, not even at the edition boundary. That's just me writing down some thoughts I've just had :slight_smile:

Surprisingly, one of the most complex aspects of rust to support in an IDE is a module and import system. The crux of the problem is that, in IDE, you want to skip over as much code as possible, and, ideally, should be able to run name resolution on a single file without looking much into other files.

The ideal case here is Java: every file starts with a package directive, so each top-level item gets an unambiguous fully-qualifed name, and it is possible to compute the set of FQNs, contributed by a single file, by looking just at this file.

The Rust is just the opposite of that to get the set of items declared in a crate, you more or less have to process all crate's files and modules simultaneously.

The rust module system is more convenient for the user, for the following reasons:

  • modules are tree-shape, and not flat
  • fine-grained visibility is also tree-shaped
  • reexports allow for building nice faรงades

The core bit that hurts here is that combination of glob-imports and reexports makes different modules depend on each other in a difficult to disentangle ways. A nice change here which might simplify things, without hurting expressiveness/ergonomics much is

First, to make palin use items into usual imports, which don't declare new items themselves.
Second, ban * from pub use items (which still act as-if redeclaring item at the given position).

With this setup, you mostly know the set of names that can be exported from module by just looking at the module's file.

Mostly, because macros can also declare top-level items, and you need expand them to figure this out... This might very well break, so some additional restrictions for top-level macros might be needed. Maybe it's just banning imports of top-level macros via *? I don't know :slight_smile:

Another thing which would help is an ability to build a module tree without expanding macros. An interesting (and probably ergonomic) way to achieve that is to require that module structure is fully inferred from the file system layout.

10 Likes

I'm wondering how the community would react to rust-analizer explicitely not supporting pub use foo::*; and clippy having a lint telling them that for optimal ide experience not to use them. That way the language retains it's expressiveness (and backwards compatibility), but people can decide for themselves to constrain themselves for the benefit of external tooling.

10 Likes

I am especially a fan of Go's approach here, where the equivalent syntax, import . "my/package" exists for the purposes of some kinds of tests but is otherwise highly discouraged. I think that glob imports should be avoided at all costs for the benefit of readability.

1 Like

Very much this. I basically never feel the need for them and have seriously considered at times to nix glob imports whole-sale at an edition boundary.

Caveat: The only case where I would use a glob import is for something like:

fn foo() {
    use MyEnum::*;
    match scrutinee {
        Variant0 => ...,
        ManyVariantsLater => ...,
    }
}

Most of the time, this is satisfied by Self::Variant0 though, so I would be fine with giving this up personally.

2 Likes

I use glob imports inside of #[cfg(test)] blocks a lot. I'll quote the book below:

#[cfg(test)]
mod tests {
    // Note this useful idiom: importing names from outer (for mod tests) scope.
    use super::*;

    #[test]
    fn test_add() {
        assert_eq!(add(1, 2), 3);
    }
}

I have rarely used glob imports across file boundaries, though.

7 Likes

My current opinion on "good" glob imports:

  • use super::*: good for "this module is like its parent". Even better with...
  • inline modules: both use super::* to import the names from the parent scope (you could argue this could be default) and pub use inline::* to re-export from the module (but getting privacy benefits of the module boundary).
  • use prelude::*: custom preludes do make sense for widely used types when using a library. Even better when it's required to pull in pub use Trait as _ reexports just for the methods (typically extension traits).
  • use extern_crate::*: I'll often use this one for small single-module (of exports) crates that have a clearly named unambiguous API.
  • use Enum::*: good for enums where you're using the variants a lot and the variants aren't ambiguous when used this way.
  • [sarcasm] use crate::*: get back edition2015 behavior! [/sarcasm]

Of what I can recall off the top of my head of IJ-Rust's behavior here, local glob imports are typically fine, but it struggles a bit for "nested" glob imports.

Due to the number of good uses of glob imports (one I didn't mention: use in macros), I don't think an outright ban is reasonable, even if just on public glob imports. A warn-by-default lint could make a lot of sense, though.

Is there any way to make the lint only apply to cross-file globs, though? Single-file globs are by far the most common "good" glob, and shouldn't pose any problem to IDEs anyway.

11 Likes

As a non-expert on this topic, I explained Rust's approach to myself like this:

Unlike e. g. Java's approach where the individual classes' package information survives in the compiled artifacts, Rust has basically a single lib.rs entrypoint were everything that needs to exist after compilation has to be mentioned explicitly โ€“ that's why Rust does not have a package directive.

I think Rust's approach is not intuitive, but I accepted it as a technical requirement due to Rust's decisions and blame my unfamiliarity.

I agree on glob imports being bad; even in languages like Java I have come to the conclusion that not only glob imports are bad, but also that classes in the same package being visible to each other without an import at all was a bad idea.

If I'm using an X that I haven't declared in the same file, it should be mandatory that X is mentioned in the imports.

2 Likes

Count me in as someone who would like clippy to discourage non-super non-enum glob imports. I find explicit importing of all non-std::prelude items one of the most important features in making unfamiliar Rust code easier to understand to humans (especially without an IDE).

2 Likes

Though it's not pub, one place I use glob imports extensively is use log::*;.

1 Like

I agree with @soc, when I started interpreting mod foo; as include "foo.h"; the system finally started to make sense.

Going back to the topic, I won't mind if pub use globs are discouraged.

Hm, but why not log::error!, log::warn!, log::info!? I think log::error! reads so much better than plain error!.

1 Like

I know that I'm never going to use another macro called info!, warn!, etc. so the log:: is just noise to me.

5 Likes

A more careful first step might be to not lint against all non-super non-enum glob imports, but to only ban pub glob imports. Judging by the OP, this seems to be worst issue with Rust IDE support.

1 Like