Avoid `pub use`

Just an advice and maybe a query for comments. At some point pub use, even pub(crate) use will clutter the namespace with ambiguities and multiple references to the same or different items.

Just always use the full import paths.

I just had to refactor all my imports and with IDEs like RustRover it was easier, but still took me 2 days for 2 projects.

An advantage is also that you can see exactly how modules interact with each other. I would also recommend importing every item individually.

At least this applies to large projects.

1 Like

a query for comments

In this spirit - one place pub use may be effectively required is to maintain semver compatibility while restructuring a crate, so that code that compiles with the original import can continue to compile when the symbol has been moved to another module.

Denying compatibility and incrementing the major version is also an option, certainly, but it's a decision the crate author should really make based on their intentions regarding compatibility in general, rather than making it based on a single refactoring.

3 Likes

Maybe for a release build you would activate something like a publicly exposed api:

lib.rs

pub mod foo;
mod bar;

// only release build
#[cfg(not(debug_assertions))]
pub use {
    foo::*,
    bar::*,
};

I've run across pub use before, with *, and as a downstream user of the crate, just browsing the source on lib.rs or github, it makes it harder to find where things are actually defined.

Also, the API of a crate shouldn't depend on debug_assertions in this way. This would force downstream users to use the qualified names anyway, if they want to run tests. (??)

6 Likes

I was looking for a way to disallow pub use imports within the same crate. Idk if there is a better compile flag to use. There doesn't seem to be something like a release flag.

Counterpoint: pub use appears in the standard library, serde, clap, rand, tokio, regex ... It's a tool for making your API nicer for users, making their imports simpler and keeping them from having to memorize your project's internal structure, and that's great.

11 Likes

Yes, its a tool for users of a crate but within the same crate it can easily get overused and make it harder to maintain. It can even break auto import features in IDEs. I would like a way to prohibit this and only use pub use to actually export items to users of a crate.

I don't think I'd do this, for at least two reasons:

  1. pub use foo::* is unlikely to exactly match the existing API I'm trying to relocate, and likely to expose a lot of symbols that then form part of the crate's public interface.

  2. The interface depending on the build flags is going to make the crate much harder for clients to work with.

What I would do, during the kinds of refactorings I have in mind, is move and alias individual symbols, unconditionally, taking - for example -

pub struct User {
  // …
}

to

mod users;

pub use users::User;
1 Like

I would rephrase this as don't pub use already pub items, which I mostly agree with.

I know people are all gung-ho about their 10kloc files now, but I prefer breaking things up as they get a bit chonky for me (a very vibes-based metric, to be fair), and pub using the members up to a more client's view of the API, even within a crate, is very nice.

6 Likes

I would say two things (both rules of thumb, which you can break if you understand why the rule exists and why you're a special case):

  1. Never pub use _::*; glob imports like this are a pain to follow without IDE support, and thus should be limited in use.
  2. (same as @simonbuchan) Avoid having multiple paths to the same item in your exported API. That means that if I can import Quux as use foo::bar::Quux, I should not be able to import it as use foo::baz::Quux as well, or even as use foo::Quux; each item should have one exported path only.

And yes, this means that I consider "preludes" where you're supposed to use c::prelude::* a problematic pattern; they're great for prototyping, when you get everything you need in one glob import, but once you come to maintain the code later, they're a pain because they make it harder to identify and fix problems caused by the same symbol now coming from two places after a version bump of a dependency.

8 Likes

There's nothing wrong with pub use.

The problem is with glob imports, which is an orthogonal discussion. You are confusing the two.

pub use in itself is a very nice tool for e.g. bringing often-used items into the crate root, in a prelude-like manner. Who says you always have to glob-import from a prelude or the crate root? You don't have to.

You can always do

use create::{often_used_function, OftenUsedType, OftenUsedTrait};

but if this means not having to remember the full path of every single item, then that's already a win. I certainly prefer this over the absurd, Java-esque dance of

use create::{
    some_module::some_submodule::often_used_function,
    helpers::irrelevant::OftenUsedType,
    oh::god::what::am::i::doing::here::OftenUsedTrait,
};

that some crates (e.g. rusqlite) make you perform.

Most of the internal structure of a crate is an implementation detail that necessarily leaks into the API. Providing some porcelain on top of the plumbing is good.

5 Likes

The thing that I find problematic with pub use is when I can write either:

use create::{often_used_function, OftenUsedType, OftenUsedTrait};

or

use create::{
    some_module::some_submodule::often_used_function,
    helpers::irrelevant::OftenUsedType,
    oh::god::what::am::i::doing::here::OftenUsedTrait,
};

and get the same result.

Using pub use to set up my exposed API so that I always write the first form is good API design, and fine.

Using pub use to give my users a choice between the two, so that I just have to know that create::OftenUsedType is actually the same thing as create::helpers::irrelevant::OftenUsedType is not fine, since you're now ensuring that once my project gets big enough, I'll see both forms, and I'll either have to spend time and effort enforcing a style rule about which one to pick, or spend time and effort dealing with people getting confused because there's two ways to write an identical thing.

But the underlying rule is not "pub use is bad" (because it's not); it's that two paths to the same item is problematic, and I should only have one path to any given item.

9 Likes

Hard disagree.

That's the antithesis of the "plumbing and porcelain" principle, which in fact is necessary if you want to provide usable software that's both easy-to-use for the simple use cases and structured enough for the more complex use-cases (and the maintainer's own sanity).

Why?

I can provide create::OftenUsedType, and then create::helpers::irrelevant::_ for other types related to OftenUsedType with no difficulty. And it's trivial to document the relationship between OftenUsedType and helpers::irrelevant::_ in both places so that it's trivial to find.

The problem with having it exposed in two places is twofold:

  1. If I look at documentation, use "jump to definition", or otherwise for create::OftenUsedType, I end up in the wrong place, and it's incredibly confusing to work out why I'm seeing what looks like the wrong thing; I've seen this confuse people to the point where they've used the wrong thing, because they wanted some_module::some_submodule::often_used_function, but because OftenUsedType is actually exported in a different place, they found helpers::irrelevant::often_useful_function with a similar type and used that instead.
  2. Plenty of tooling, including rust-analyzer, doesn't understand fully that these are the same thing, and I've seen more than one file where someone using rust-analyzer has used all three of use create::OftenUsedType and a bare OftenUsedType, qualified path of create::OftenUsedType, and qualified path of create::helpers::irrelevant::OftenUsedType. The tooling will turn all the create::OftenUsedTypes into bare OftenUsedTypes, because it sees the use statement, but it does not offer to do the same for create::helpers::irrelevant::OftenUsedType, because the tooling does not recognise that this is the same thing exported under a different name.

Between the two, my experience is that multiple paths to the same thing confuse more often than they help; there are exceptions to this, where the extra path is useful (for example, cross-crate re-exports), but as a guiding rule, one path to each item reduces confusion, and IME does not make the complex use cases any harder - nor does it make the maintainer's sanity worse if lib.rs contains pub use crate::helpers::irrelevant::OftenUsedType and irrelevant.rs contains struct OftenUsedType instead of pub struct OftenUsedType.

1 Like

I totally agree! I forgot which crate that was, but there was one crate I depended on that was particularly guilty of this. I would write some code, then rely on IntelliJ IDEA to find the correct import, and then it would offer me two options from that crate, forcing me to investigate what was going on, when one was just the re-use of the other.

Anything public should be in packages that make sense to the user, and are not inspired by implementation details. Public items should only move if the users think there is a better location, not when the implementation changes.

4 Likes

And note that this is why pub use is needed. I should be able to put the thing itself in an implementation-dependent place, using pub use to rearrange my exported API to make a good API for users.

If, for example, the right place to put an item from an implementation point of view is crate::fec::viterbi, but the user expects to see it in crate::wireless, I should be able to implement it as a private item in crate::viterbi::decoder, and use pub use to "move" it across to crate::wireless for public consumption.

4 Likes

And now if you want to expose the underlying mechanism as well, you'll make the user import the bits and pieces from the crate root and an unrelated, deep path. Yuck, gross.

As opposed to your preference, where I just have to magically know somehow that OftenUsedType, SpeciallyNamedTypeBecauseThisModuleIsOdd, foo::helpers::OftenUsedType, foo::legacy_interactions::SpeciallyNamedTypeBecauseThisModuleIsOdd and foo::Prelude::CommonOftenUsedType are all actually the same thing, because only foo::helpers::OftenUsedType is the actual definition, and the others are pub use aliases for it.

Both are gross, but I find the one where I have to know that multiple things with different names are not actually different a lot nastier to deal with than the one where when I write my use statements at the top of my module (with IDE assist), I have to have one for use foo::OftenUsedType, and another for use foo::helpers::function_used_with_type.

1 Like

I don't actually find verbose imports that bad. The only reason it could be bad would be because its a lot to type but with IDE assist that is not an issue at all. And it actually provides some insight into what modules the file is interacting with.

1 Like

the amount of typing isn't actually the problem, it's knowing what to type. most of the time, the internal module structure of a crate is irrelevant, and you just want to import a handful of types. sometimes it makes sense for larger projects, especially those with feature flags, but in a lot of cases it makes sense to just expose the core types at the crate root.

3 Likes