-
Notifications
You must be signed in to change notification settings - Fork 12
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
Clean up imports. #106
Clean up imports. #106
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These all seem like sensible things to change (and thanks for the detailed write-up!)
I would add that I believe most of these things happen here not for lack of knowledge, but mostly due to a lack of enforcement through clippy
/ linting on this kind of stuff.
The WhySo the rule of thumb (i; and this is not standardized) followed in regards to imports is to make things as easy as possible for someone new / else to read. I dont optimize for line length or conciseness, i try to add as much context with as little chars as sensibly possible. So whenever absolute imports are a) short and b) add context and c) resolve ambiguity i personally prefer absolute imports like Also many crates like I personally find My ConcernsLast nit: Citing your example Also a genuine question i have: How would you, when moving on with this approach, solve the problem where you have two structs, that do the same, but come from two crates, like Last last nit: When using aliases, it is harder to keep them consistent in a large codebase, compared to absolute imports; So this means we would sometimes have Consensus NoteAlso personal opinions; Im happy to agree to the general consensus |
I tend to agree with Mara on the points raised above, and the general sentiment from Patrick. My own decision criteria wrt to imports is the following:
AliasesIn particular, the reason why I consider aliases bad is because they are a source of surprise, and minimising surprise is a good way to counter code entropy. What this actually means in practice is that when one sees ShadowingShadowing names from Note on line widthI think worrying about line width doesn't really make sense in Rust, not only because modern screens have plenty of space for 120+ characters even on split-screen setups, but also because |
Awesome points from everyone! I feel like I agree with everything that's been said. I'll try to change this PR to be in line with the sentiment and apply the suggestions from Mara. |
So, two things: First: I have applied all of the suggestions made in here. Maybe someone look them over to make sure I did not miss anything? It took me a bit to address some of the things here. Second: coding style is something that is very fascinating. Everyone has their own workflow and axioms that allow them to be productive. I feel that I'm constantly evolving and picking up new ideas. I hope it is not too annoying to talk about it and I understand what you optimize for. I wanted to address some statements and questions that I forgot to address previously:
I think that is a fair point. Also, in this case the ergonomic benefit is not major. It probably depends on who you are optimizing for. For a casual reader of the code,
Interacting with both Otherwise, this would be a case for where renaming would be the most sensible thing to do, for me at least. use reqwest::Response as ReqwestResponse;
use axum::request::Response as AxumResponse; But there are more considerations:
use axum::{
extractor::{State, Headers},
request::{Response, IntoResponse},
http::StatusCode,
Json
};
// turn reqwest response into axum response
fn convert(response: reqwest::Response) -> Response {
// ..
} I tend to like using aliases when I can use them to convey meaning beyond the absolute path of the crate. For example, to convey the intent. Check out this code, where I am using both use tokio::sync::Mutex as AsyncMutex;
use std::sync::Mutex as SyncMutex;
use hashbrown::HashMap;
// with aliases: convery meaning, remove noise.
fn get_cache() -> AsyncMutex<HashMap<ImString, SyncMutex<CacheData>>> {
// ..
}
// without aliases: more explicit, but adds unneccessary implementation details
// (we don't really care that the mutex is from tokio, we don't care that the hashmap is from
// hash brown, they are both just abstractions)
fn get_cache() -> tokio::sync::Mutex<hashbrown::HashMap<imstr::sync::ImString, std::sync::Mutex<CacheData>>> {
} What's nice about this is that I can go ahead and swap out the type I use for the AsyncMutex for another crate without needing to mess with the code. I feel that there are some corner cases like this where aliases are actually more helpful than using the absolute path and lead to more understandable code.
As I said, aliases are something that are sprinkled conservatively into the code base ideally. I have not had issues with them being inconsistent. Through code review you see other people's style and imitate it. But if they are not, it is also not too bad, as long as the meaning is conveyed. For example, when some of the code base uses I would much rather read But I do agree that they should not be overused. It's actually rather rare that I need to use import aliases. Instead, something I like to do is create type aliases in the crate like this: pub type Database = deadpool::managed::Pool<postgres_tokio::non_blocking::Database>; And then just
For this I would say: ideally, when I read code I don't really care if a Most people have some kind of a setup using
There is some truth to this, and the missing
Errors due to shadowing are very rare, because the compiler catches nearly all of them. Some APIs are specifically designed so that it is safe to import them and shadow built-in stuff. For example, if you import Damn this thing turned into an essay. |
88b6f7b
to
247dbc9
Compare
I actually agree that aliasing adds value here, but I also think this is a good argument against shadowing
I don't think it actually does, as evidenced by my mistake when reviewing your other PR, it's easy to forget which implementation we're dealing with, especially when we're used to using the
I'm not sure I agree with that statement. If you accidentally use a
While I agree the namespace doesn't add semantics when used like this, that is confounding its purpose, which is to disambiguate. Aliasing for the sake of clarifying what a symbol represents is a valid, but orthogonal, use-case, which has more to do with compensating for bad API design outside of our control than something that should be regular practice. In fact it is perfectly plausible that if you adhere to the practice of renaming web clients to
I get that realistically you wouldn't mix and match third-party web clients like this, but it would be perfectly sane to have a local HttpClient that wraps an external one, in which case you end up with a similar situation.
I'm fully in agreement with this approach, in fact I'll argue that we probably run into valid use-cases for type declarations like this more often than import aliases. As a final note, I'm actually not particularly concerned about mixing styles when it comes to this. Code style consistency is important, but If you feel strongly about Buffrs having a consistent style, I suggest you put up a proposal for project guidelines and we can do a vote. Once ratified I'd be happy to go with whatever style is most popular (though enforcing may be tricky). |
I have just rebased this on to of the current |
Does that mean all issues with this PR are resolved? |
This commit "cleans up" the code base somewhat by making sure that types, functions and modules that are referred to multiple times in the code are properly imported, instead of referring to them by their absolute path.
To me, this makes code more readable. I like to keep visual noise to the minimum. There are some cases where it makes sense to use an absolute path multiple times, for example to make code less ambiguous. But for the most part, I try to avoid it.
Unqualified vs qualified types
In Rust, we have the choice between using types in a qualified way:
and using them in an unqualified way, after importing:
Both of these options produce valid code. I have some personal preferences for this, which kind of relate to my development workflow. I tend to (have) to read a lot of code, and as such I try to optimize for code that I find easy to read.
I also tend to develop in the terminal, with a split pane, such that only half my screen is used for my editor of choice.
With these two constraints, there comes some preferences:
Shorter lines of code are more legible for me (< 80 or 100 lines, depending on terminal and screen)
Less repetition is better for me. If I work in a file that talks to an HTTP API, then I know what the
Client
is, I do not need it spelled out asreqwest::Client
.On the other hand, if I'm reading in a file that handles a HTTP API and a database connection, then it might make sense to disambiguate the
postgres::Client
from thereqwest::Client
. Although typically I would solve that by renaming the import, for example:In the example code above it might be clearer to import
serde_json::Value
asJsonValue
.In the example code above it might be clearer to import
serde_json::Value
asJsonValue
if that is how it is used, because that way it more clearly communicates the intent (this is a JSON value) without being overly explicit (serde_json::Value
), because when reading the code, I might not particularly care that it is a JSON value from theserde_json
crate (as I can get that information easily fromrust-analyzer
), I just care about the meaning of this type, which is to hold arbitrary parsed JSON data.As a result of this, my personal preference is that when in doubt, I will import things. I will always explicitly import things if they are used multiple times in the same file. Sometimes, I will import things and rename them to make it more clear.
There is one thing to note here: A lot of people use VSCode with
rust-analyzer
. The default setup for this will show the full, qualified path for types. So, if you are used to using VSCode or other IDEs, you will not really notice a difference between writingClient
versusreqwest::Client
, because VSCode will show you thereqwest::
path anyways (in gray). This means that if you are used to VSCode, you may tend to write out the full, unqualified paths just because you are used to reading code that way. VSCode can be told to not show the full paths unless you are hovering over them, if you want to try that. I do feel that there is still a benefit from keeping code short, even if it does not make a difference to some.Error handling
When using error handling libraries such as
anyhow
oreyre
, I tend to import theirResult
type and use that in an unqualified way (as in, useResult<T>
rather thaneyre::Result<T>
).Why? In those crates, Result is defined as:
This means that
eyre::Result<T>
is just anstd::result::Result<T, E>
with theE
defaulting toeyre::Report
. For that reason, importingeyre::Result
and using it asResult
is fine, since you can always override the error type. In other words,eyre::Result<A, B>
andstd::result::Result<A, B>
are equivalent, the only different with theeyre
version is that if you do not specify the error type, it falls back to being aneyre::Report
.Since projects typically do not mix and match several error handling libraries in the same crate, whenever you see
Result<T>
it is immediately obvious that this will be aneyre
result, or whichever error handling library the current project uses. So writing outeyre::Result
does not add any necessary context for understanding the code, unless perhaps there are multiple error libraries in use in the current file.Logging
In general, glob imports are discouraged a little bit, it is always nice to not use them so that it is immediately obvious what comes from where. For that reason, there is a clippy lint that can be used to disallow using glob imports.
Logging or tracing is the one thing where I feel it can make sense to do a glob import:
The reason is that:
tracing::
. Tracing or logging statements should be short and sweet. There is nothing shorter and sweeter thaninfo!("XYZ")
.info!(...)
anywhere in the code and expect it to work, and not have to edit the import to addinfo
to the list because you had only imported{debug, error}
before.log
crate and thetracing
crate easy, as they both have similar macros.Deriving Traits
In the PR, I have also removed implementations of
PartialOrd
andOrd
and replaced them with derives. I believe that they were maybe added in error, because it was not know that they could be derived automatically.My rule of thumb is:
Other notes
Note that this PR includes strong (personal) opinions, which you may or may not agree with. A part of the reason for creating this PR is to spark a conversation about coding styles and preferences, so feel free to chime in and tell me why I'm wrong! Also feel free to close this. Writing up these style preferences is a lot of fun, makes you think about what preferences you have and why you might have them.
I hope this all makes sense