Skip to content

Coding Rules for Rust: Types & Traits #233

@rcseacord

Description

@rcseacord

Do any of these make sense as rules?

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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.

  1. 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)];

  1. 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.

  1. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions