Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cleanup internal imports #243

Merged

Conversation

konstin
Copy link
Member

@konstin konstin commented Jul 22, 2024

Imports now only come from two modules: crate, which contains all public symbols, and crate::internal, which contains all the internal symbols. I'm surprised the split works out so cleanly. I used nightly rustfmt to group the imports.

All internal apis are now pub(crate). Some of these will become public again in the uv fork.

I used my ide's lints to remove unnecessary path prefixes and cleaned some other nitpicks on the way.

No functional changes, this is a maintenance-only PR.

konstin added 3 commits July 22, 2024 21:02
Imports now only come from two modules: `crate`, which contains all public symbols, and `crate::internal`, which contains all the internal symbols. I'm surprised the split works out so cleanly.

All internal apis are now `pub(crate)`. Some of these will become public again in the uv fork.

I used nightly rustfmt to group the imports.
Comment on lines -137 to -138
#[cfg(feature = "serde")]
type SemVS = Range<SemanticVersion>;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a bunch of errors caused by that feature-gated import in the past, so now i inlined it

Comment on lines -504 to +500
indexes_to_remove in prop::collection::vec((any::<prop::sample::Index>(), any::<prop::sample::Index>(), any::<prop::sample::Index>()), 1..10)
indexes_to_remove in vec((any::<Index>(), any::<Index>(), any::<Index>()), 1..10)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We were already using plain vec in other places

@Eh2406
Copy link
Member

Eh2406 commented Jul 22, 2024

Two follow up thoughts. If there are nightly FMT ways to preserve this consistent formatting, can we add it to our CI runs? Similarly, if there are clippy lints encouraging pub(crate) should we turn them on? Like in general this worked out so well that I want to preserve it.

use crate::solver::DependencyProvider;
use crate::type_aliases::{IncompDpId, Map};
use crate::version_set::VersionSet;
use crate::{DependencyProvider, DerivationTree, Map, NoSolutionError, VersionSet};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well this is exactly the kind of cyclic imports that I’ve always wanted to avoid. crate imports internal.core which in turn imports crate. I believe this lead into putting definitions and functions in a much less organized and maintainable way in the long term, because we don’t care where we put it and just pub(crate) it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do find everything at the root very useful and convenient for outside callers of the library, or test files. Just not for the inside of the library itself.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've reverted the internal import changes

Copy link
Member

@mpizenberg mpizenberg Jul 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the long wait time. I didn’t mean specifically internal just inside the library in general. The idea is to avoid cyclic imports, which are signs that something is not compartmentalized properly. Because a module that requires cyclic imports means that any other module that would need a functionality from this one, would also end up with the whole crate (all its modules) as a dependency. Which makes things like dead code elimination more complex, and other bad architecture proofs in general.

So basically, I find it fine to have examples, benchmarks, external tests, ... to import everything from the new top level crate. But I’d like to avoid having the library modules (anything that is in the import path from the root lib.rs) to import crate module.

That being said, I know I’m a bit stubborn on this, and I know this isn’t something very common in rust crates, so I’m willing to bend if you all think its better.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've never gotten the hang of keeping my imports well structured. So I personally would not mind making it a cyclical ball of spaghetti. :-P On the other hand, I'm terrible at naming things and I never remember to fix documentation. This project is better for your efforts to rein in those bad habits of mine.

So whichever way we decide, I would deeply appreciate automation that insists on it. Something in CI, even just a grep for the "bad" patterns, or are there tools in the ecosystem for identifying cyclical imports.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cyclical ball of spaghetti sounds delicious! ahah. I wanted to explain my reasoning because every time something is cyclical, it makes everything more complex. Dependency solving is more complex, memory releasing for garbage collector is more complex, etc. I see no reason why import graph solving would not suffer from similar issues, potentially leading to longer compilation times or less optimal dead code elimination for compiled bundles.

But that’s just my hunch and it isn’t backed by any scientific proof or concrete testing using Rust and using our code base in particular. So take that with a grain of salt. And if you think it makes code reading and maintenance simpler I can’t argue against that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For concrete data, I guess compiling a rather simple example based on pubgrub could be used. To see if this change makes any impact on compile time and bundle sizes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that I think about it, it probably would not change anything for pubgrub, because its not like people would use only a subset of pubgrub, they basically would use one of the top level modules that do need everything in the lib. There isn’t any part of this lib that really stands on its own.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolving symbols to the source location is comparatively simple for a compiler: You build an alias map from exported symbol to origin path or the local definition for each file once and then when looking up a symbol, you iterate over the alias until you find the source location. If i want to resolve Foo, i walk through the scopes until i find a use crate::Foo, then i ask crate/src/lib.rs for Foo, it has memorized it as coming from crate::foo/src/foo.rs due to pub use foo::Foo, then i go to crate::foo/src/foo.rs and it tells me it has a local stuct Foo, so i get my fully qualified path crate::foo::Foo (and pub/priv checking along the way).

I found cyclical imports often become unavoidable with more complex crates, e.g. between the error capturing some struct for context and that struct return the error in its methods. I haven't found cyclical imports being a signal for bad architecture.

There are something that are known to be costly to the compiler, notably large/many dependencies, proc macros (need to be compiled, executed and then rustc needs to compile the bulk of generated code), generics (rustc generates and processes monomorphized code for each instantiation, i.e. n different usages give us n times the code) and llvm in general.

I say we structure the code in the way that's easiest for us to maintain and move as much work as possible to the compiler. Pubgrub compile ridiculously fast compare to any other crate i've worked on (0.2s for incremental cargo build, and ~2s for cargo test on my desktop) so i have zero worries about compile times.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the detailed answer! Alright, don’t forget to "unrevert" your changes then.

@konstin konstin added this pull request to the merge queue Jul 31, 2024
Merged via the queue into pubgrub-rs:dev with commit 42ad7ea Jul 31, 2024
4 checks passed
@konstin konstin deleted the konsti/pubgrub-cleanup-internal-imports branch July 31, 2024 11:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants