Rust Code Simplifier
Proactive pattern guide for writing simple, idiomatic Rust. Apply these patterns whenever writing or modifying Rust code — not only when asked to simplify.
Project rules: These patterns align with .claude/rules/rust-patterns.md. When in doubt, re-read the rules file.
1. Iterator Chains Over Manual Loops
Use iterator chains for transformation logic. Reserve for loops only when the body needs early error returns with ?.
// WRONG
let mut names = Vec::new();
for tool in tools {
if tool.is_available() {
names.push(tool.name().to_owned());
}
}
// RIGHT
let names: Vec<String> = tools
.iter()
.filter(|t| t.is_available())
.map(|t| t.name().to_owned())
.collect();
When to keep a loop: The body calls fallible operations with ?:
// Loop is correct here — early return on error
for entry in entries {
let meta = entry.metadata().context("failed to read metadata")?;
process(&meta).await?;
}
2. Option/Result Chaining
Flatten nested match/if let on Option/Result into chained combinators or ?.
// WRONG — nested match pyramid
match config.project.as_ref() {
Some(project) => match project.settings.as_ref() {
Some(settings) => match settings.get("key") {
Some(val) => Some(val.to_owned()),
None => None,
},
None => None,
},
None => None,
}
// RIGHT — ? operator for Option propagation
config
.project
.as_ref()?
.settings
.as_ref()?
.get("key")
.map(|v| v.to_owned())
Combinator quick reference:
| Pattern | Use when |
|---|---|
? |
Propagate None/Err to caller |
.map() |
Transform inner value |
.and_then() |
Chain operations that return Option/Result |
.unwrap_or_default() |
Fallback to Default impl |
.unwrap_or_else(|| ...) |
Fallback with computation |
3. Early Returns
Guard against invalid/empty states at the top. Avoid deep nesting for the main logic.
// WRONG — main logic buried in nested conditions
fn process(entries: &[Entry], prefix: &str) -> Vec<String> {
let mut results = Vec::new();
if !entries.is_empty() {
if !prefix.is_empty() {
for entry in entries {
if entry.name.starts_with(prefix) {
results.push(entry.name.strip_prefix(prefix).unwrap().to_owned());
}
}
}
}
results
}
// RIGHT — guard clauses, then flat logic
fn process(entries: &[Entry], prefix: &str) -> Vec<String> {
if entries.is_empty() || prefix.is_empty() {
return Vec::new();
}
entries
.iter()
.filter(|e| e.name.starts_with(prefix))
.filter_map(|e| e.name.strip_prefix(prefix))
.map(|s| s.to_owned())
.collect()
}
Signals to look for: is_empty(), is_none(), is_err(), boolean flags checked before the real work begins.
4. Avoid Redundant Clones
Borrow instead of cloning. When ownership is needed, use .to_owned() (not .clone()). Use String::from("...") for string literals (not "...".to_string()).
// WRONG — clones where borrows suffice
fn format_tool(tool: &Tool) -> String {
let data = tool.data.clone();
let name = tool.name.clone();
format!("{}: {}", name, data)
}
// RIGHT — format! borrows automatically
fn format_tool(tool: &Tool) -> String {
format!("{}: {}", tool.name, tool.data)
}
// WRONG
let label = "default".to_string();
let copy = config.clone();
// RIGHT
let label = String::from("default");
let copy = config.to_owned();
5. if let for Single-Variant Match
Use match only when both arms carry logic. Use if let when only one variant matters.
// WRONG — match with throwaway arm
match result {
Some(val) => process(val),
None => (),
}
// RIGHT
if let Some(val) = result {
process(val);
}
// RIGHT — match when both arms carry logic
match result {
Ok(ctx) => tool.format(ctx).await,
Err(e) => log_and_report(e),
}
Common Mistakes
| Mistake | Fix |
|---|---|
.clone() to satisfy borrow checker |
Restructure to borrow, or use .to_owned() if ownership needed |
"literal".to_string() |
String::from("literal") |
Nested if/match guarding empty/none |
Early return guard clause at top |
for loop that only filters + maps |
Iterator chain with .filter().map().collect() |
match x { Some(v) => f(v), None => () } |
if let Some(v) = x { f(v); } |
Nested match on Option chains |
? operator or .and_then() |