Open cole-miller opened 2 months ago
Thanks for the fantastic feedback, Cole! These are excellent points and I've now carefully gone through each, which has resulted in a good number of improvements to the document as a result.
Not 100% necessary but you could mention --all-features here.
This seems like a good addition, done.
A couple of the code snippets use Unicode curly quotes (single and double), it might be better to replace them with ASCII quotes so the code will compile when pasted elsewhere.
Excellent catch! I've gone through and made sure curly quotes aren't used in code snippets
Might be missing context on my part but this sentence read strangely to me because I almost never read or write code that binds a name to a closure. Typically the closure is just passed as a literal to the function that wants it (Iterator::map, thread::scope, etc.). That said, in situations where you do bind a name to a closure (maybe to re-use it?) this seems like good advice.
Just a thought: if there are no captures and you're binding a name to the closure, should you consider just defining a fn instead? (If visibility is a concern I personally think fn within fn is fine when used tastefully.)
This is a good point. This section has now been reworked
This initially struck me as too prescriptive but after thinking about it for a bit I can see the argument. If you use this style consistently the total number of distinct identifiers in your program goes down and you can form a stronger association between identifiers and meanings, so there's more room in your working memory when reading code. Nice!
Exactly! I find keeping the names consistent like this tends to make the concepts flow together really nicely too!
This brings up a question: for acronyms in type names, do we use ASTQueryMatch or AstQueryMatch? The standard library seems to prefer the former, see TcpStream.
My mistake, thanks for the note! This now uses the standard convention
I was slightly surprised to discover that if you use SomeEnum::* in the middle of a function, the bare variant names are visible throughout the function, not just after the use. (Playground demo.) That makes me feel a little torn about this advice---I agree that sometimes you really need this kind of glob import to make your matches readable, but if use is going to affect an entire block I want to see it at the top of that block.
Oh wow, this is surprising! I've changed the prose here to advise putting the use
at the top and to keep the scope small.
Missing the match in this snippet.
Oops! Thank you!
TIL Clippy can enforce this one: https://rust-lang.github.io/rust-clippy/rust-1.56.0/index.html#rest_pat_in_fully_bound_structs
Ah interesting! I'm not sure this is quite the same case though, I think this is making sure that no ignore marker ignores nothing, rather than enforcing that all fields are considered (e.g. with ignore markers). I think these are opposite sides of the same coin
Nit: should this say "reference" instead of "pointer"?
Ah that's better, yes
Typo: Config vs. ServerConfig.
Thank you!
Nit: would "...when naming associated types in expressions" be clearer? (Strong +1 to the actual guideline here.)
This is a very nice way of putting this. I've amended to the above
Sometimes it's not a temporary storage location---rustc can in some cases implicitly put the thing being referenced in static storage and give you a 'static reference back. This works with e.g. integer literals but also sometimes with more complicated stuff, and it can be handy, for example this code in std. See also this thread by a bunch of Rust wizards that digs into the nuances.
Ah, I wasn't aware of this, thanks for the extra context! I've changed the wording here to reflect this
Ah, you anticipated the thing I wrote above :) I think I would personally prefer shadowing when using this idiom just because an extra level of nesting (and two additional lines) feels like a stiff price to pay, but I can appreciate that the explicit scope helps out too.
The two extra lines is a little stiff, but I think shadowing doesn't strongly highlight enough the scope of where mutation can occur. I think it's quite easy to miss a let x = x;
(this line also looks a little strange to my eyes). There may be a middleground we can reach here, so it could be interesting to get more opinions here
Micro-nit: the snippets look like they're doing blocking file I/O in an async function, which is best avoided and might distract from the point they're intended to illustrate.
This is a document of micro-nits :D You make a very good point, I've updated the snippet to be less distracting
Just for counterpoint, some disadvantages I can think of to using field getters instead of public fields:
- Loss of pattern matching
- Getters that return references don't support borrow splitting
- Extra code And I could be wrong, but I think struct initialization supports all the same
coercions as function calls (reference).
Hmmm, there's a little more nuance required in this section, I think. I've updated it to try to emphasise the difference between structs which represent part of the model and those which just group together data for transfers. I think this section definitely needs a little work though
I really like this section, but it feels a little out of place here because it's so specific. Maybe it could be generalized to a broad guideline for when to define functions and types within a fn body?
I see what you mean. This section has now been generalised
This reads to me as ambiguous on whether it's okay to use anyhow::Error in a binary crate.
For now, this is intentional. I think it can occasionally be convenient, but the lack of types is at least concerning and can make it difficult to abide by the rest of the error-handling advice if sections of a binary crate are moved into a library crate. My gut feeling is that consistency across all crate types would be best here, but I'm still undecided
I totally agree about the advantages of enumerated errors, but I want to offer a counterpoint to this. If your library exposes all the underlying errors from its public Error type, you're turning making internal details of your library a public API promise. Sometimes this is what you want, but not always. For example, you might have a library that makes some HTTP requests using reqwest as an implementation detail. In that case you probably don't want to put expose reqwest::Error from your error type, because that prevents you from switching to a different HTTP client (or even a new incompatible version of reqwest) without bumping your library's major version.
This also applies to impl From
for MyError, since that becomes part of the public API in the same way (handy as it is for ?). So I think a balance has to be struck between transparency and opacity by weighing the tradeoffs in each case.
This is a very good point. I've now added advice on a newtype pattern which hides dependency errors and thus avoids implementation detail leakage
Panics are recoverable within the same thread using catch_unwind, but the point stands because that goes out the window if your crate is built with panic = "abort" (or if you panic while another panic is unwinding the stack).
Aha, thanks for this! I've now generalised the wording to cover this case too.
Shout it from the rooftops! I always enforce this in my code with #![deny(elided_lifetimes_in_paths)].
Aha! It looks like these standards may be in need of some rust tooling config options...
Missing "in"?
Ah yes
Nit: I would frame it as enabling additional powers rather than turning off checks, as the book does.
Consistency is definitely for the best here, I've updated the framing
Might there be other items in this category, like a central trait that ties the library together?
This is a tough one, it's quite difficult to define what 'tying a library together' means concretely enough here. Also, central traits may come with implementations on standard types, which could start to make these files quite large. I think consistent separation would be better
On the topic of crate-specific Result aliases, would it make sense to recommend always importing them as somecrate::Result, so that std::result::Result is not shadowed?
Somehow shadowing std::result::Result
is a good thing as it means that by default, in our crate we're using our crate's own-brand errors and results. This way, when we do need to use the standard Result
type, we're forced to go to a little extra effort, which helps highlight that something different from the rest of the codebase is taking place
I read through this and it looks great! Comments on the sections that I had thoughts about are below, interleaved with quotes to make it clear what I'm referring to. Some are just noting my strong agreement with a particular point (or that you changed my mind), some are nits, some are providing a counterpoint to the position in the document (which you may have considered and rejected already).
Not 100% necessary but you could mention
--all-features
here.A couple of the code snippets use Unicode curly quotes (single and double), it might be better to replace them with ASCII quotes so the code will compile when pasted elsewhere.
Might be missing context on my part but this sentence read strangely to me because I almost never read or write code that binds a name to a closure. Typically the closure is just passed as a literal to the function that wants it (
Iterator::map
,thread::scope
, etc.). That said, in situations where you do bind a name to a closure (maybe to re-use it?) this seems like good advice.Just a thought: if there are no captures and you're binding a name to the closure, should you consider just defining a
fn
instead? (If visibility is a concern I personally thinkfn
withinfn
is fine when used tastefully.)This initially struck me as too prescriptive but after thinking about it for a bit I can see the argument. If you use this style consistently the total number of distinct identifiers in your program goes down and you can form a stronger association between identifiers and meanings, so there's more room in your working memory when reading code. Nice!
I like this section!
This brings up a question: for acronyms in type names, do we use
ASTQueryMatch
orAstQueryMatch
? The standard library seems to prefer the former, seeTcpStream
.I was slightly surprised to discover that if you
use SomeEnum::*
in the middle of a function, the bare variant names are visible throughout the function, not just after theuse
. (Playground demo.) That makes me feel a little torn about this advice---I agree that sometimes you really need this kind of glob import to make your matches readable, but ifuse
is going to affect an entire block I want to see it at the top of that block.Missing the
match
in this snippet.TIL Clippy can enforce this one: https://rust-lang.github.io/rust-clippy/rust-1.56.0/index.html#rest_pat_in_fully_bound_structs
Nit: should this say "reference" instead of "pointer"?
+1, this seems like a good set of distinctions.
Typo:
Config
vs.ServerConfig
.Nit: would "...when naming associated types in expressions" be clearer? (Strong +1 to the actual guideline here.)
I'm not sure I understand how this exception relates to the guideline. In the example below that uses a
Result
alias, the associated type is named in the function signature, not in an expression.Big fan of this section, no notes :)
In the "good" snippet, should we prefer the type annotation to a turbofish? (I go back and forth on this myself.)
Good point, hadn't considered this!
As an alternative to introducing a nested scope, what about shadowing?
Sometimes it's not a temporary storage location---rustc can in some cases implicitly put the thing being referenced in static storage and give you a
'static
reference back. This works with e.g. integer literals but also sometimes with more complicated stuff, and it can be handy, for example this code in std. See also this thread by a bunch of Rust wizards that digs into the nuances.Ah, you anticipated the thing I wrote above :) I think I would personally prefer shadowing when using this idiom just because an extra level of nesting (and two additional lines) feels like a stiff price to pay, but I can appreciate that the explicit scope helps out too.
This section answers another of the questions I wrote above! Agree with the guidance, thanks for spelling it out.
I've always been mildly averse to
.ok()
as an idiom for ignoring errors, but this made me rethink that position. After all, it's pretty much the same as.map_err(|_| ())
, in the sense thatOption<T>
is isomorphic toResult<T, ()>
. So +1 on this.Agree, one additional benefit of
Ok(())
is that some diffs are cleaner when you use it, e.g.vs.
Micro-nit: the snippets look like they're doing blocking file I/O in an async function, which is best avoided and might distract from the point they're intended to illustrate.
Just for counterpoint, some disadvantages I can think of to using field getters instead of public fields:
And I could be wrong, but I think struct initialization supports all the same coercions as function calls (reference).
I might consider broadening this, I think there are other cases where "all fields public" is the right choice, with the common thread being that these types are just containers for data that don't have invariants. This blog post shaped my thinking on this.
I really like this section, but it feels a little out of place here because it's so specific. Maybe it could be generalized to a broad guideline for when to define functions and types within a
fn
body?Strong +1.
This reads to me as ambiguous on whether it's okay to use
anyhow::Error
in a binary crate.I totally agree about the advantages of enumerated errors, but I want to offer a counterpoint to this. If your library exposes all the underlying errors from its public
Error
type, you're turning making internal details of your library a public API promise. Sometimes this is what you want, but not always. For example, you might have a library that makes some HTTP requests usingreqwest
as an implementation detail. In that case you probably don't want to put exposereqwest::Error
from your error type, because that prevents you from switching to a different HTTP client (or even a new incompatible version ofreqwest
) without bumping your library's major version.This also applies to
impl From<DependencyError> for MyError
, since that becomes part of the public API in the same way (handy as it is for?
).So I think a balance has to be struck between transparency and opacity by weighing the tradeoffs in each case.
An example might be good for this one.
Panics are recoverable within the same thread using
catch_unwind
, but the point stands because that goes out the window if your crate is built withpanic = "abort"
(or if you panic while another panic is unwinding the stack).Good criterion!
I don't have a super strong opinion on this but there's an argument to be made that generic type parameters at least are important for understanding an API (because they trigger you to think about monomorphization and using the same parameter name in analogous contexts helps tie the code together).
Definitely +1.
Shout it from the rooftops! I always enforce this in my code with
#![deny(elided_lifetimes_in_paths)]
.Missing "in"?
I already linked this blog post above but it's relevant here too. For structs that do have a mix of field visibilities though, this is a good guideline.
I was going to object to this by saying that the handwritten
Ord
/PartialOrd
implementation gets nasty when you have more than a few fields, but then I realized you had an example of how to do it concisely above :)Nit: I would frame it as enabling additional powers rather than turning off checks, as the book does.
Well put!
Another item that I was going to object to, and then on thinking about it realized that it was making a good point :)
Might there be other items in this category, like a central trait that ties the library together?
I'm glad that you also stan mod.rs :)
On the topic of crate-specific
Result
aliases, would it make sense to recommend always importing them assomecrate::Result
, so thatstd::result::Result
is not shadowed?