| name | modern-rust-expert |
| description | Expert knowledge for writing cutting-edge, idiomatic Rust code with Rust Edition 2024, strict clippy compliance, and functional-but-pragmatic philosophy. Use when writing any Rust code, fixing clippy warnings, structuring modules or crates, reviewing or refactoring Rust code, or questions about Rust 2024 features, async patterns, documentation standards, or performance optimization. |
Modern Rust Expert
Expert knowledge for writing cutting-edge, idiomatic Rust code with Rust edition 2024, strict clippy compliance, and functional-but-pragmatic philosophy.
When to Use This Skill
Automatically apply this knowledge when:
- Writing any Rust code in this project
- Fixing clippy warnings
- Structuring new modules or crates
- Reviewing or refactoring Rust code
Rust Edition 2024 Specifics
Version Requirements
- Edition: 2024
- MSRV: 1.85.0 (minimum for edition 2024)
- Always check
rustc --versionand update withrustup update stableif needed
Edition 2024 & Rust 1.85+ Features Available
1. Async Functions in Traits (Stable!)
Use async fn directly in trait definitions without async-trait crate:
// Modern Rust - async fn in traits (no macro needed!)
trait Database: Send + Sync {
async fn save(&self, data: &[u8]) -> Result<(), Error>;
async fn load(&self, id: &str) -> Result<Vec<u8>, Error>;
}
// Implementation
impl Database for PostgresDb {
async fn save(&self, data: &[u8]) -> Result<(), Error> {
// Direct async implementation
sqlx::query("INSERT INTO ...").execute(&self.pool).await?;
Ok(())
}
}
Use this for: All async trait methods in Environment traits (Database, HttpClient, etc.)
2. Return Position Impl Trait in Traits (RPITIT)
Return impl Trait from trait methods:
trait EventPublisher {
// Can return impl Future instead of Box<dyn Future>
fn publish(&self, event: Event) -> impl Future<Output = Result<()>> + Send;
}
Benefit: No heap allocation, better performance than Box<dyn Future>.
3. Let-Else Statements
Early return with pattern matching:
// Modern pattern for error handling
fn process_order(action: OrderAction) -> Result<Order, Error> {
let OrderAction::PlaceOrder { customer_id, items } = action else {
return Err(Error::InvalidAction);
};
// customer_id and items are in scope here
Ok(Order { customer_id, items })
}
// Compare to old style:
fn process_order_old(action: OrderAction) -> Result<Order, Error> {
match action {
OrderAction::PlaceOrder { customer_id, items } => {
Ok(Order { customer_id, items })
}
_ => Err(Error::InvalidAction),
}
}
Use for: Extracting enum variants with early return.
4. Enhanced Const Generics
Use const generics in more contexts:
// Const generic for effect buffer size
struct EffectBuffer<Action, const N: usize> {
effects: [Option<Effect<Action>>; N],
}
impl<Action, const N: usize> EffectBuffer<Action, N> {
const fn new() -> Self {
Self {
effects: [const { None }; N],
}
}
}
Use for: Stack-allocated buffers with configurable size.
5. Inline Const Expressions
Use const { } blocks in const contexts:
const DEFAULT_CAPACITY: usize = 16;
struct Cache<T> {
// Inline const expression
data: [Option<T>; const { DEFAULT_CAPACITY * 2 }],
}
6. C-String Literals
Create CStr at compile time:
// Modern: c"string" literal
let path = c"/tmp/data"; // Type: &'static CStr
// Old way required:
use std::ffi::CString;
let path = CString::new("/tmp/data").unwrap();
Use for: FFI code and system calls.
7. If/While Let Chains
Combine multiple patterns with &&:
// Modern: let chains
fn handle_event(state: &State, action: Action) {
if let OrderAction::PlaceOrder { items, .. } = action
&& !items.is_empty()
&& state.can_place_order()
{
// All conditions met
}
}
// Old way required nested ifs or match
8. Improved Pattern Matching
Rest patterns in slices:
match events.as_slice() {
[first, .., last] => {
// first and last are available
}
_ => {}
}
Patterns in let statements:
let [first, second, ..] = &events[..] else {
return Err(Error::NotEnoughEvents);
};
9. Expanded Const Fn Capabilities
More operations allowed in const fn:
const fn calculate_capacity(base: usize) -> usize {
// These are all allowed in const fn now:
if base < 16 { 16 } else { base }
}
const fn create_default<T>() -> Option<T> {
None // Can return generic types
}
// Use in const contexts
const CAPACITY: usize = calculate_capacity(10);
10. Precise Capturing in RPIT
Control which lifetimes are captured:
trait Store {
// Only capture 'a, not all lifetimes
fn get_state<'a>(&'a self) -> impl Future<Output = State> + use<'a>;
}
Features NOT Available (Require Nightly)
To avoid confusion, these are NOT in stable yet:
- ❌
genblocks for generators - ❌ Type alias impl trait (TAIT)
- ❌ Specialization
- ❌ Generic const expressions (full support)
- ❌
#[derive]onenumwith generics in some cases
Recommended Patterns for This Project
- Use async fn in traits - No more
async-traitdependency - Prefer let-else - Cleaner error handling
- Use RPITIT - Return
impl Futureinstead ofBox<dyn Future> - Const generics for buffers - Stack allocation where possible
- Let chains - Combine multiple conditions elegantly
Clippy Configuration & Compliance
Workspace Lint Configuration
Always configure lints at workspace level in root Cargo.toml:
[workspace.lints.rust]
unsafe_code = "forbid"
missing_docs = "warn"
[workspace.lints.clippy]
# Pedantic lints - USE LOWER PRIORITY so specific lints can override
pedantic = { level = "warn", priority = -1 }
# Deny common issues
unwrap_used = "deny"
expect_used = "deny"
panic = "deny"
todo = "deny"
unimplemented = "deny"
# Performance
missing_const_for_fn = "warn"
# Cognitive complexity
cognitive_complexity = "warn"
# Documentation
missing_errors_doc = "warn"
missing_panics_doc = "warn"
Critical: Lint groups like pedantic MUST have priority = -1 to avoid conflicts with specific lints.
Individual Crate Configuration
In each crate's Cargo.toml, inherit workspace lints:
[lints]
workspace = true
Common Clippy Issues & Solutions
1. Mixed Attributes Style
Problem: Having both outer (///) and inner (//!) doc comments on the same item.
Wrong:
/// Module for actions
///
/// This contains action types.
pub mod action {
//! Action implementations
}
Correct:
/// Module for actions
///
/// This contains action types.
///
/// Action implementations.
pub mod action {}
Rule: Pick one style per item. For modules, use outer docs (///) and move inner content to the outer doc comment.
2. Documentation Backticks
Rule: ALL type names, function names, trait names in documentation MUST be in backticks.
Wrong:
/// - Database: The database trait
/// - HttpClient: HTTP client
Correct:
/// - `Database`: The database trait
/// - `HttpClient`: HTTP client
3. Missing # Panics Documentation
If a function can panic, document it:
/// Create a test clock
///
/// # Panics
///
/// Panics if the hardcoded timestamp fails to parse
/// (should never happen in practice).
#[allow(clippy::expect_used)] // Justified in test utilities
pub fn test_clock() -> Clock {
// ...
}
4. Non-Debug Types
Some types (like Future) don't implement Debug. Manually implement it:
pub enum Effect<Action> {
Future(Pin<Box<dyn Future<Output = Option<Action>> + Send>>),
}
// Manual Debug implementation
impl<Action> std::fmt::Debug for Effect<Action>
where
Action: std::fmt::Debug,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Effect::Future(_) => write!(f, "Effect::Future(<future>)"),
// ... other arms
}
}
}
5. Missing const fn
If clippy suggests a function can be const, make it const:
// Clippy will suggest this can be const
pub const fn merge(effects: Vec<Effect<Action>>) -> Effect<Action> {
Effect::Parallel(effects)
}
6. Wildcard Imports
Avoid wildcard imports in library code:
Wrong:
use super::*;
Correct:
use super::{Arc, Effect, Reducer, RwLock};
Exception: Wildcard imports are acceptable in tests and examples.
7. Match Arm Consistency
Consolidate identical match arms:
Wrong:
match effect {
Effect::None => {},
Effect::Parallel(_) => {},
Effect::Sequential(_) => {},
}
Correct:
match effect {
Effect::None | Effect::Parallel(_) | Effect::Sequential(_) => {
// Placeholder implementation
},
}
8. Doc List Indentation
Module docs must be indented or separated with blank lines:
Wrong:
pub mod mocks {
//! Mock implementations
}
Correct (moved to outer):
/// Mock implementations
pub mod mocks {}
Rustfmt Configuration
Stable Features Only
Only use rustfmt features available in stable Rust. Many formatting options require nightly.
Safe rustfmt.toml:
edition = "2024"
max_width = 100
hard_tabs = false
tab_spaces = 4
newline_style = "Unix"
fn_params_layout = "Tall"
match_block_trailing_comma = true
chain_width = 60
use_try_shorthand = true
use_field_init_shorthand = true
force_explicit_abi = true
Avoid (require nightly):
imports_granularitygroup_importswrap_commentsformat_code_in_doc_commentsnormalize_commentsreorder_impl_itemsbrace_stylematch_arm_blocks
Functional-but-Pragmatic Philosophy
Core Principles
- Prefer immutability, but allow
&mut selfwhen performance matters - Prefer pure functions, but recognize async/await patterns
- Prefer composition, but allow practical escape hatches
- Favor readability over theoretical purity
Practical Applications
Effect-as-Value Pattern
Describe side effects as values, don't execute them immediately:
// Good: Return effect description
fn reduce(&self, state: &mut State, action: Action, env: &Env) -> Vec<Effect> {
vec![Effect::Database(SaveOrder), Effect::PublishEvent(event)]
}
// Bad: Execute side effects directly
fn reduce(&self, state: &mut State, action: Action, env: &Env) {
env.database.save(state); // ❌ Side effect in reducer!
}
Mutable State in Reducers
It's OK to mutate state in place in reducers for performance:
trait Reducer {
fn reduce(
&self,
state: &mut Self::State, // ✅ Mutable for performance
action: Self::Action,
env: &Self::Environment,
) -> Vec<Effect<Self::Action>>;
}
This is pragmatic: tests are still fast and deterministic, and we avoid unnecessary cloning.
Static Dispatch Over Dynamic
Prefer generic types (static dispatch) over trait objects (dynamic dispatch):
// Prefer this (static dispatch, zero-cost)
struct Store<S, A, E, R>
where
R: Reducer<State = S, Action = A, Environment = E>
{
reducer: R,
}
// Over this (dynamic dispatch, runtime cost)
struct Store<S, A, E> {
reducer: Box<dyn Reducer<State = S, Action = A>>,
}
Exception: Use dynamic dispatch when you need runtime polymorphism (plugins, hot-swapping implementations).
Error Handling
Strict Rules:
- NEVER use
unwrap()orexpect()in library code (deny by clippy) - NEVER use
panic!()in library code (deny by clippy) - NEVER leave
todo!()orunimplemented!()in production code (deny by clippy)
Exceptions (must be explicitly allowed):
// Test utilities can use expect with justification
#[allow(clippy::expect_used)]
pub fn test_clock() -> Clock {
DateTime::parse_from_rfc3339("2025-01-01T00:00:00Z")
.expect("hardcoded timestamp should always parse")
.with_timezone(&Utc)
}
Always document why you're allowing it.
Code Organization Patterns
Module Documentation
Pattern 1: Small modules - Use outer docs only:
/// Action types and utilities
///
/// Actions represent all possible state transitions.
pub mod action {}
Pattern 2: Large modules - No inner docs if you have comprehensive outer docs:
/// Store runtime for coordinating reducer execution
///
/// Detailed explanation here.
///
/// Store runtime implementation details.
pub mod store {
// No //! needed, everything is in outer docs
}
Re-exports
Use pub use for convenience, but document what you're re-exporting:
// Re-export commonly used types from dependencies
pub use chrono::{DateTime, Utc};
pub use serde::{Deserialize, Serialize};
// Re-export from submodules for convenience
pub use mocks::{FixedClock, test_clock};
Workspace Structure
For multi-crate projects:
project/
├── Cargo.toml # Workspace root with [workspace.dependencies]
├── core/
│ ├── Cargo.toml # Uses workspace.dependencies
│ └── src/lib.rs
├── runtime/
│ ├── Cargo.toml # Depends on core
│ └── src/lib.rs
└── testing/
├── Cargo.toml # Depends on core + runtime
└── src/lib.rs
Rules:
- Share dependencies via
[workspace.dependencies] - Use
dependency.workspace = truein crate manifests - Shared package metadata via
[workspace.package] - Workspace lints via
[workspace.lints]
Common Gotchas & Solutions
1. Workspace Dependencies Cannot Be Optional
Wrong:
[workspace.dependencies]
sqlx = { version = "0.8", optional = true } # ❌ Error!
Correct:
[workspace.dependencies]
sqlx = { version = "0.8" } # Define without optional
# In individual crate Cargo.toml:
[dependencies]
sqlx = { workspace = true, optional = true } # ✅ Make it optional here
2. Edition 2024 Requires Newer MSRV
If you get:
error: rust-version 1.83.0 is older than first version (1.85.0) required by edition 2024
Solution: Update MSRV:
[workspace.package]
rust-version = "1.85.0" # Minimum for edition 2024
3. Future Doesn't Implement Debug
Pin<Box
4. Async Functions in Placeholders
Placeholders in async functions trigger unused_async:
#[allow(clippy::unused_async)] // Justified: placeholder implementation
async fn execute_effect(&self, effect: Effect<A>) {
// Will be implemented in Phase 1
}
Testing Patterns
Test Organization
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_something() {
// Unit test
}
#[tokio::test]
async fn test_async_something() {
// Async test
}
}
Property-Based Testing
Use proptest for property tests:
use proptest::prelude::*;
proptest! {
#[test]
fn property_holds(input: Vec<u8>) {
prop_assert!(check_property(&input));
}
}
Performance Considerations
Zero-Cost Abstractions
With static dispatch, generics compile to optimal code:
// This generic code:
fn reduce<D: Database>(state: &mut State, db: &D) {
db.save(state);
}
// Monomorphizes to:
fn reduce_with_postgres(state: &mut State, db: &PostgresDatabase) {
db.save(state); // Direct call, no vtable lookup
}
Allocation Minimization
In hot paths, consider SmallVec for small collections:
use smallvec::SmallVec;
// Stack-allocated for ≤ 4 items, heap for more
fn reduce(&self, ...) -> SmallVec<[Effect; 4]> {
let mut effects = SmallVec::new();
effects.push(Effect::Save);
effects
}
Documentation Standards
Crate-Level Documentation
Start lib.rs with comprehensive module docs:
//! # Crate Name
//!
//! Brief description.
//!
//! ## Overview
//!
//! Detailed explanation.
//!
//! ## Example
//!
//! ```ignore
//! // Example code here
//! ```
Function Documentation
/// Brief one-line description
///
/// Longer detailed explanation if needed.
///
/// # Arguments
///
/// - `state`: Description
/// - `action`: Description
///
/// # Returns
///
/// What the function returns.
///
/// # Errors
///
/// When this function returns an error (if applicable).
///
/// # Panics
///
/// When this function panics (if applicable).
///
/// # Example
///
/// ```
/// // Example usage
/// ```
pub fn my_function(state: &State, action: Action) -> Result<(), Error> {
// ...
}
Type Documentation
/// Brief description of the type
///
/// # Type Parameters
///
/// - `S`: State type
/// - `A`: Action type
///
/// # Example
///
/// ```
/// // Example usage
/// ```
pub struct MyType<S, A> {
// ...
}
Quick Reference Checklist
Before committing Rust code:
- Run
cargo fmt --all - Run
cargo clippy --all-targets --all-features -- -D warnings - Run
cargo test --all-features - Run
cargo doc --no-deps --all-features - All type names in docs have backticks
- No
unwrap/panic/todoin library code - Functions that panic have
# Panicssection - No wildcard imports (
use super::*) - Manual
Debugimpl for non-Debug types - Const fn where applicable
- Static dispatch preferred over dynamic
Common Commands
# Format code
cargo fmt --all
# Check without building
cargo check --all-features
# Build
cargo build --all-features
# Test
cargo test --all-features
# Lint
cargo clippy --all-targets --all-features -- -D warnings
# Documentation
cargo doc --no-deps --all-features --open
# Documentation with warnings as errors
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features
Project-Specific Guidelines
This Project's Philosophy
- Functional Core, Imperative Shell: Pure business logic, effects as values
- Explicit over Implicit: All side effects are visible
- Type Safety: Make invalid states unrepresentable
- Testability: Business logic tests run at memory speed
- Performance: Zero-cost abstractions via static dispatch
- Pragmatism: Functional patterns, but practical when needed
Architecture Patterns
- Action: Unified type for commands and events
- Reducer: Pure function
(State, Action, Env) → (State, Effects) - Effect: Values describing side effects (not execution)
- Environment: Trait-based dependency injection
- Store: Runtime that coordinates everything
When in Doubt
- Check the architecture spec:
specs/architecture.md - Follow patterns from existing code
- Prefer explicitness over cleverness
- If clippy complains, there's usually a good reason
- Ask for clarification rather than guessing
Remember: This skill is automatically applied to all Rust code in this project. Follow these guidelines to write idiomatic, performant, and maintainable Rust code.