-
Notifications
You must be signed in to change notification settings - Fork 28
Description
Do any of these make sense as rules?
- Prefer Explicit, Intent-Revealing Type Definitions
Rule: Use type aliases or newtypes (tuple structs) when they increase clarity or strengthen type safety.
Use a newtype when two quantities have the same underlying primitive but different semantics:
struct Meters(u32);
struct Seconds(u32);
Use a type alias when the semantic meaning is exactly the same as the underlying type:
type UserId = u64;
Rationale: Prevents accidental mixing of logically distinct values, improves safety, and assists API discovery.
- Avoid Using impl Trait in Public Signatures (Except for Return Types)
Rule: In public APIs, do not use impl Trait in argument position; prefer explicit trait bounds.
Allowed (return position):
pub fn iter_names() -> impl Iterator<Item = String>;
Avoid in parameters:
// Less clear
pub fn process(x: impl Read) { ... }
// Preferred
pub fn process<R: Read>(x: R) { ... }
Rationale: Makes type relationships explicit and helps prevent breaking API changes.
- Use Traits to Represent Capabilities, Not Categories
Rule: A trait should describe behavior, not a “type group.”
Bad example:
trait Vehicle {} // no behavior
Good example:
trait Drivable {
fn drive(&mut self, km: u32);
}
Rationale: Traits are for polymorphism and abstraction of behavior, not tagging or inheritance.
- Keep Traits Small and Focused
Rule: Prefer small cohesive traits (“single responsibility”). Avoid large, multipurpose trait collections.
Good:
trait Serialize { fn serialize(&self) -> String; }
trait Deserialize: Sized { fn deserialize(s: &str) -> Self; }
Avoid:
trait SerdeLike {
fn serialize(&self) -> String;
fn deserialize(s: &str) -> Self;
fn version(&self) -> u32;
...
}
Rationale: Allows flexible composition, easier mocking, and fewer breaking changes.
- Use Trait Bounds Conservatively
Rule: Do not over-constrain types in generics.
Avoid:
fn compute<T: Clone + Debug + Display>(x: &T) { ... }
Use only what you need:
fn compute<T: Clone>(x: &T) { ... }
Rationale: Reduces unnecessary coupling and makes the function more widely applicable.
- Prefer #[derive] for Simple Traits
Rule: Where correct, use deriving for traits such as:
Copy, Clone
Debug
PartialEq, Eq
PartialOrd, Ord
Hash
Default
Example:
#[derive(Debug, Clone, PartialEq, Eq)]
struct Point { x: i32, y: i32 }
Rationale: Derivations are consistent, tested, and faster to maintain.
- Only Implement Copy for Types With Clear "Copy Semantics"
Rule: A type should implement Copy only if copying it is clear, cheap, and intuitive.
Good candidates: numeric types, small POD structs.
Avoid for types that own resources (Vec, String, file handles, network sockets).
Rationale: Avoids surprising implicit duplication of data or resource operations.
- Use Associated Types in Traits When There Is Only One Logical Output Type
Rule: Prefer associated types over type parameters when the implementer should fix one concrete type.
Example:
trait Graph {
type Node;
fn neighbors(&self, node: Self::Node) -> Vec<Self::Node>;
}
Rationale: More ergonomic; prevents type-parameter explosion.
- Use Enums to Represent State Variants Explicitly
Rule: Prefer enumerations over implicit states represented by Option, booleans, or magic values.
Bad:
struct Connection {
connected: bool,
}
Good:
enum ConnectionState { Connected, Disconnected }
struct Connection {
state: ConnectionState,
}
Rationale: Enums encode invariants directly into the type system.
- When Defining Error Types, Implement the Standard Error Traits
Rule: Your error type should implement:
Debug (required)
Display
std::error::Error
Example:
#[derive(Debug)]
enum MyError {
Io(std::io::Error),
Parse(String),
}
impl std::fmt::Display for MyError { ... }
impl std::error::Error for MyError {}
Rationale: Integrates cleanly with ?, anyhow, eyre, and error-chains.
- Avoid Exposing Type Invariants Through pub Fields
Rule: Most types should encapsulate internal fields unless the invariant is trivial.
Avoid:
pub struct Range { pub start: i32, pub end: i32 }
Prefer:
pub struct Range { start: i32, end: i32 }
impl Range {
pub fn new(start: i32, end: i32) -> Result<Self, RangeError> { ... }
}
Rationale: Prevents invalid states and helps maintain invariants.
- Use From/Into for Conversions, Not Ad Hoc Methods
Rule: Use standard conversion traits instead of custom to_xxx() or from_xxx() names.
Example:
impl From<u32> for Duration {
fn from(secs: u32) -> Self { Duration::from_secs(secs as u64) }
}
Rationale: This leverages the universal conversion ecosystem.
- Do Not Overuse Trait Objects (dyn Trait)
Rule: Only use trait objects for runtime polymorphism, not simply to avoid generics.
Bad:
fn process(items: Vec<Box<dyn Display>>) { ... }
Better:
fn process<T: Display>(items: Vec<T>) { ... }
But use dyn Trait when you need heterogeneity:
let tasks: Vec<Box<dyn Task>> = vec![Box::new(ReadTask), Box::new(WriteTask)];
- Ensure Trait Methods Form a Coherent Contract
Rule: A trait should specify behavioral invariants explicitly—even if only in documentation.
Example:
/// # Contract:
/// `pop()` must remove and return the *last* item.
/// `push()` must add an item to the *end* of the collection.
trait Stack { ... }
Rationale: Prevents ambiguous or surprising implementations.
- Prefer Safer Type Representations (Avoid unsafe Struct Layout Unless Necessary)
Rule: Avoid repr(C) or repr(packed) unless absolutely required for FFI or specific memory layout.
Packed structs remove alignment guarantees and can break soundness if improperly referenced.