Ordering discipline
General definition ordering
When read from top to bottom, a file should feel like a tour of the APIs it defines.
The most important items should be defined further up the file, with their helpers below.
No impl
block should come before the type or trait to which it relates.
In this way, lower-level implementation details are hidden from the reader until they wish to know more, at which point, having gained a good knowledge of the overall form of the code, they can read on to understand how it functions.
✅ Do this:
type Result<T> = std::result::Result<T, Error>;
enum Error {
Invalid,
NetworkUnavailable,
MalformedEnvUrl {
env_var: &'static str,
source: Box<dyn std::error::Error>,
},
Unsupported,
Unknown(Box<dyn std::error::Error>),
}
impl<E> From<E> for Error
where
E: std::error::Error + 'static,
{
fn from(err: E) -> Self {
Error::Unknown(Box::new(err))
}
}
struct Arbitrary;
type Foo = Arbitrary;
impl Foo {
pub fn some_func(&self) {
self.some_helper_func();
}
fn some_helper_func(&self) {
// ...
}
}
⚠️ Avoid this:
type Result<T> = std::result::Result<T, Error>;
enum Error {
Invalid,
NetworkUnavailable,
MalformedEnvUrl {
env_var: &'static str,
source: Box<dyn std::error::Error>,
},
Unsupported,
Unknown(Box<dyn std::error::Error>),
}
impl<E> From<E> for Error
where
E: std::error::Error + 'static,
{
fn from(err: E) -> Self {
Error::Unknown(Box::new(err))
}
}
struct Arbitrary;
type Foo = Arbitrary;
impl Foo {
fn some_helper_func(&self) {
// ...
}
pub fn some_func(&self) {
self.some_helper_func();
}
}
Impl block placement
By default, impl SomeType
blocks for a given type should be in the same file, immediately below where that type is defined.
Trait implementations impl SomeTrait for SomeType
should go in the file where SomeTrait
or SomeType
is defined.
This is effectively the orphan rule but applied within crates.
Impl block ordering
The impl
blocks in the same file for MyType
should be ordered as follows:
impl MyType
unsafe impl StandardTrait for MyType
unsafe impl MyTrait for MyType
unsafe impl ThirdPartyTrait for MyType
impl StandardTrait for MyType
impl MyTrait for MyType
impl ThirdPartyTrait for MyType
The impl
blocks in the same file for MyTrait
should be ordered as follows:
impl MyTrait for StandardType
impl MyTrait for MyType
impl MyTrait for ThirdPartyType
Derive ordering
Put all derive items in a single #[derive(...)]
(the formatter will preserve readability by introducing line-breaks as it deems fit).
Derived items should be ordered as follows:
Copy
Clone
Debug
PartialEq
Eq
PartialOrd
Ord
- Other standard traits, ordered lexicographically
- Third party traits, ordered lexicographically
Declaration ordering
Rust provides several different types of declaration and where these are declared consecutively in the same block, they should be ordered for visual consistency. This will help draw the reader’s eye to the important parts of each declaration, rather than getting lost in some superfluous ordering.
Declarations should be ordered as follows:
const
static
lazy_static!
let
let mut
Struct field ordering
The more public a field, the more likely a user will to want to know more about it and understand it. Therefore, we should put the items they are most likely to care about nearer the top of our code, avoiding them having to skip over parts uninteresting to them. Specifically, this means that we should place:
pub
fields first,pub(crate)
fields next,- private fields last.
Structs neatly organised in this way make it clear when the reader has entered implementation details and hence when they are less likely to glean useful information.
If the reason for ordering fields in a different way to the above is due to a derivation such as Ord
or PartialOrd
, this is not a good reason for deviation from the norm.
Maintaining consistency is of a higher priority than a single derivation, hence the relevant implementations should be written out by hand.
The reader is more likely to be looking at the entire struct rather than just one trait implementation.
✅ Do this:
use std::collections::BTreeMap;
type StackFrame = ();
type Value<'h> = &'h ();
struct ScriptExecutionContext<'h, T> {
pub user_data: T,
pub(crate) global_vars: BTreeMap<String, Value<'h>>,
stack: Vec<StackFrame>,
max_steps: Option<u64>,
steps: u64,
}
⚠️ Avoid this:
use std::collections::BTreeMap;
type StackFrame = ();
type Value<'h> = &'h ();
struct ScriptExecutionContext<'h, T> {
stack: Vec<StackFrame>,
pub(crate) global_vars: BTreeMap<String, Value<'h>>,
pub user_data: T,
steps: u64,
max_steps: Option<u64>,
}