Simplicity Over Cleverness: Why Idiomatic Rust Beats Over‑Engineered Abstractions
Share this article
Simplicity Over Cleverness: Why Idiomatic Rust Beats Over‑Engineered Abstractions
The phone buzzes at 3 AM.
You roll out of bed, open your laptop, and see this in the logs:
thread 'tokio-runtime-worker' panicked at 'called `Result::unwrap()` on an `Err` value: Error("data did not match any variant of untagged enum Customer at line 1 column 15")', src/parsers/universal.rs:47:23You open the codebase and find this:
pub struct UniversalParser<T: DeserializeOwned> { format: Box<dyn DataFormat>, _marker: std::marker::PhantomData<T>, } impl<T: DeserializeOwned> UniversalParser<T> { pub fn parse(&self, content: &str) -> Result<Vec<T>, Box<dyn std::error::Error>> { self.format.parse(content) } }A few thoughts rush through your head: What the hell is a
PhantomData? Why is there a trait object? The error is buried somewhere in the interaction between thatDataFormattrait, the generic parser, and serde deserialization.The stack trace is 15 levels deep. It’s like peeling an onion… it makes you cry.
You run
git blameand curse the colleague who wrote this code. Whoops, it was you a few months ago.This isn’t just a story about a bug. It’s a cautionary tale about the seductive pull of cleverness in Rust.
The Rust Design Myth: If it’s possible, it should be used
Rust is often marketed as a playground for infinite possibility. That optimism is well‑deserved: the language offers zero‑cost abstractions, powerful generics, and a compile‑time safety net that can catch bugs before they hit production. But the same features that make Rust so expressive also make it easy to over‑engineer.
“We love to stretch Rust to its limits.” – Matthias Endler, Idiomatic Rust.
When developers write code that looks correct but is conceptually over‑complicated, they create accidental complexity. This is not the inevitable complexity of a problem, but the extra layers of indirection added by the author’s own design choices.
Generics: A Double‑Edged Sword
Generics are a core Rust feature, but they come with a cost:
- Compile‑time bloat – each instantiation monomorphizes the function, producing a separate copy of the code.
- Readability hit – signatures balloon, error messages become cryptic, and the mental model of the API expands.
Consider a simple string‑processing helper. A naïve implementation is straightforward:
fn process_user_input(input: &str) {
// …
}
If you later decide to support String as well, you might be tempted to write:
fn process_user_input(input: impl AsRef<str>) {
// …
}
Now the function is generic, but you also inherit two monomorphized versions (for &str and String). Add lifetimes, Send + Sync, and you’re looking at a signature that reads like a cryptic puzzle.
“The problem is so simple, so how did that complexity creep in?” – Endler.
The lesson: only make something generic if you need to switch the implementation now. Premature generalization is a recipe for future pain.
Trait Objects and Dynamic Dispatch
Trait objects (Box<dyn Trait>) are a powerful tool for runtime polymorphism, but they hide the concrete type behind a vtable. In the UniversalParser example, the DataFormat trait object adds a layer of indirection that the stack trace reveals. If the only formats you actually support are CSV and JSON, a simple enum or separate concrete types might be clearer and faster.
Why Simplicity Matters
Reliability
Simple systems have fewer moving parts to reason about. A single failure point is easier to locate, test, and fix. In the 3 AM crash above, the failure chain was 15 levels deep; a simpler design would have surfaced the error earlier.
Performance
Contrary to popular belief, simple code is often faster. The compiler can optimize a straightforward loop more aggressively than a complex generic chain. Rust’s quick_sort example shows that a concise, recursive implementation can be as efficient as the standard library’s sort_by for many workloads.
Maintainability
When new developers join a project, they read code, not documentation. A small, well‑named function is far easier to understand than a monolithic module full of trait objects and phantom markers. Simplicity reduces cognitive load, which is critical when teams grow or when knowledge silos form.
Practical Guidelines
- Start with a naïve implementation. Write the simplest code that satisfies the requirement.
- Refactor only when you see a pattern. If you’re repeatedly writing the same boilerplate, that’s a sign you might need an abstraction.
- Measure before you optimize. Premature performance tweaks often add complexity without measurable benefit.
- Keep the user in mind. Libraries should expose the common case first. Optional, advanced features can be added later.
- Avoid “performance crimes” for the sake of speed. Use
ArcorBoxonly when the cost of allocation justifies it. - Document your abstractions. Good documentation turns a complex API into a mental model that developers can trust.
The Human Side of Rust
Rust’s safety guarantees can make developers hyper‑aware of memory usage. This vigilance sometimes turns into an over‑optimization mindset, where every allocation is scrutinized. While the language’s performance is impressive, the real cost of complexity is human. A tangled codebase slows onboarding, increases bug surface area, and erodes morale.
“The code you write for companies will be application code, not library code.” – Endler.
Application code rarely needs the full power of Rust’s abstraction toolkit. Focus on clarity, not cleverness.
Final Thought
The 3 AM crash was a wake‑up call. It reminded us that Rust’s elegance can become its own trap. By embracing simplicity, we not only build more reliable systems but also create code that future developers—whether they’re seasoned Rustaceans or newcomers—can read, understand, and extend.
“Be simple.” – Matthias Endler.
Source: Matthias Endler, Idiomatic Rust, https://corrode.dev/blog/simple/