Guidance around reexporting #176
Replies: 15 comments
-
@sanmai-NL So you're specifically talking about re-exporting foreign items from dependencies rather than re-exporting local items in the same crate under a more convenient path. Personally, I think creating public dependencies by re-exporting foreign items should be at least mildly discouraged since in my experience it creates a future maintenance burden that outweighs the convenience of the re-export in the first place. As an example, I recently had to back out of a The In cases where you do determine |
Beta Was this translation helpful? Give feedback.
-
@KodrAus Hmmm, I think this issue is starting with the assumption that you have a public dependency for one reason or another, and then trying to answer a question about re-exports given that you already have a public dependency. I think the termcolor thing is an example where you could avoid the public dependency in the first place, so is probably in a different class of API design than what this issue is trying to tackle (if I've understood correctly). |
Beta Was this translation helpful? Give feedback.
-
@BurntSushi Ah I was approaching this from the angle of deciding whether or not to create a public dependency by re-exporting foreign items, rather than whether you should re-export the crate for your existing public dependencies. So are we looking at whether you should re-export the crate // pub use dependency; <- should we do this in the crate root?
pub fn pub_fn(pub_arg: dependency::Arg) { ... } // pub use dependency; <- should we do this in the crate root?
pub struct PubStruct { ... }
impl dependency::Trait for PubStruct { ... } |
Beta Was this translation helpful? Give feedback.
-
My proposal is to encourage re-exporting the crate for your existing public dependencies. |
Beta Was this translation helpful? Give feedback.
-
I think there is a bit of "it depends", even in this guideline (which may be why it was removed). For instance, these are cases of having a public dependency where I'm not sure it makes sense to re-export the crate:
|
Beta Was this translation helpful? Give feedback.
-
I think yes in all cases. Fundamentally, this best practice is about predictability, not about judging how public dependencies C are going to be used by consumers A via crate B. Indeed, that’s what makes this best practice easy to adopt: there’s no subjectivity. Also, though somewhat orthogonal, we can write a few words about how reexporting a public dependency materially increases API surface of your crate, but since that part of the API is of a different nature then the ‘intrinsic API’ (for lack of a better, simpler word?), this does not matter. Different API stability guarantees may apply to the ‘extrinsic API’ (reexported public dependency crates). It should be possible to add a lint that checks whether all public dependency crates are reeexported. Meaning all dependency crates with any item making up the extrinsic API of the linted crate. If so, this best practice would lend itself to automated checking. |
Beta Was this translation helpful? Give feedback.
-
I see this mostly as a user experience problem than anything. One approach would be reexporting as mentioned but what if you had two dependencies which depended on futures but depended on different versions of futures. Would mixing foo::futures::Future and bar::futures::Future be an improvement or cause more UX problems? Has anyone explored the idea of cargos knowledge of transient dependencies filling in ( implicitly ) these dependencies as first class dependencies of application crates. I know there's already work underway with the 2018 edition to remove extern crate in source files. If this were the case, having cargo fill these in would be a nicer ux for application crates. If hyper depends on futures exposing it in public interfaces then an application depending on hyper may also depend on futures without an application crate needing to spell that out manually in thier cargo.toml. |
Beta Was this translation helpful? Give feedback.
-
@softprops, as before you’d have to careful what you import and how. Before, you could still import crates at incompatible versions. |
Beta Was this translation helpful? Give feedback.
-
Doesn't the practice of consuming re-exports instead of direct imports tend to reinforce or increase the likelihood of multiple versions of the same dependency within the complete project dependency graph? Duplicates are always better avoided when possible, right? Or is the proposal here to always give the opportunity by re-exporting, with the hope that consumers are thoughtful about when to use them? For a practical example of how I arrived at this theory: I started off with a "hyper 0.10 application" for which, I have since come to realize, is better understood as a "tokio (reform) 0.1.x" application with an additional hyper (now 0.12) dependency. Should that subtle/conceptual distinction influence the choice to use re-exports over direct dependencies in an application? I think it always will be influential in practice. |
Beta Was this translation helpful? Give feedback.
-
@dekellum:
Yes. 🙂 But let me get this straight: Cargo resolves dependencies based on constraints. If there is a single compatible version of, say, Do we have the same understanding here? |
Beta Was this translation helpful? Give feedback.
-
Thanks @sanmai-NL, I think my original concern regarding duplicates exists but wasn't well stated. More on that later. Back to your proposal, I would also prefer By comparison, |
Beta Was this translation helpful? Give feedback.
-
I think the key issue is that if I have crate On the other hand, if The question of whether there's another version of |
Beta Was this translation helpful? Give feedback.
-
Did you mean "when I construct |
Beta Was this translation helpful? Give feedback.
-
@grncdr Yep. |
Beta Was this translation helpful? Give feedback.
-
Had this exact same idea, even opened an issue at clippy I think an exception could be made if a type from a crate implements a trait that is fundamentally useless on its own. E.g. you probably want an executor for a But I still prefer to reexport the crate in that case. One could wish to implement the logic for trait without other crates and the change required is minimal. Also note one important thing: once a type from another crate appears in the public interface then reexporting it will not add any new requirements on backwards compatibility. There's no way one could backwards-compatibly reimplement the type later because the fact that the type is the same as one from another crate becomes a part of the API! This is a good reason for people to think twice before adding an external type to public interface in the first place. Once it's there, the crate should be reexported. |
Beta Was this translation helpful? Give feedback.
-
Guidance around reexporting has been removed. I propose to pick up this issue again, with high priority, since it has a potentially large impact on, at least, the open source Rust ecosystem, as the common practice around this issue greatly influences:
There are number of questions to resolve around this.
pub use
vs.pub extern
. When to use the one over the other. Whether to ever use them both for the same items.Draft best practice
If your crate has a public dependency, then the corresponding crate should be reexported by your crate (via
pub extern
). First of all, why consider reexporting anything? The main motivation is to help consumers C of your API in crate B avoid having to depend on your own dependencies, e.g. A, in order to work efficiently with your crate B. Would you have failed to reexport, then:It is better to use
pub extern
and preserve crate A’s structure directly in your crate B’s root rather than selectively exporting items from A viapub use
at an idiosyncratic path:pub use
. You risk not being able to name items as you prefer within your own crate at some point in the evolutions of your crate and the dependency crate.Beta Was this translation helpful? Give feedback.
All reactions