Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
221: The Visitor Pattern in Rust
You've likely hit this wall before: you have a complex data structure—maybe an Abstract Syntax Tree (AST) or a nested UI component tree—and you find yourself needing to perform a dozen different operations on it. One day it's rendering to HTML, the next it's a JSON export, and then suddenly you need a word counter or a spell checker.
The Bloated Struct Problem
The intuitive way to handle this is to just add methods to your structs. If you have a Paragraph struct and an Image struct, you give them both a render() method. It feels clean at first. But then you add export_to_pdf(), calculate_layout(), and validate_accessibility(). Suddenly, your data definitions are buried under hundreds of lines of behavioral logic that have nothing to do with what the data is and everything to do with what you're doing with it.
// This gets messy fast
struct Paragraph { text: String }
impl Paragraph {
fn render(&self) { /* ... */ }
fn export_pdf(&self) { /* ... */ }
fn word_count(&self) -> usize { /* ... */ }
// Imagine 10 more of these
}
I've seen this lead to "God Objects" where a single file becomes a nightmare to maintain because every single feature request requires modifying the core data structures. You're violating the Single Responsibility Principle, and in Rust, this often leads to awkward lifetime struggles or borrowing conflicts when the logic for one operation clashes with the needs of another.
When Enums Hit a Wall
Since we're in Rust, your first instinct is probably to use an enum and a big match statement. For a lot of cases, that's actually the right move. If your set of types is closed—meaning you know you'll only ever have Paragraph, Image, and Table—a match statement is idiomatic and fast.
But here's the rub: what if you're writing a library? You provide the AST, but you want the user of your library to be able to define their own new operations without having to modify your enum source code. You can't just tell your users to "add a match arm to the library's internal enum." That's impossible. You need a way to add new behaviors to existing types without changing those types.
Decoupling Logic with the Visitor Trait
This is where the Visitor pattern saves us. Instead of the data structure knowing how to "render" or "count," the data structure simply knows how to "accept" a visitor. The visitor is a separate trait that defines a method for every single concrete type in your structure.
Look at how this shifts the responsibility. The Element trait just says "I can be visited," and the Visitor trait says "I know how to handle every type of element."
trait Visitor {
fn visit_paragraph(&mut self, p: &Paragraph);
fn visit_image(&mut self, i: &Image);
}
trait Element {
fn accept(&self, visitor: &mut dyn Visitor);
}
struct Paragraph { text: String }
impl Element for Paragraph {
fn accept(&self, visitor: &mut dyn Visitor) {
visitor.visit_paragraph(self);
}
}
struct Image { url: String }
impl Element for Image {
fn accept(&self, visitor: &mut dyn Visitor) {
visitor.visit_image(self);
}
}
Now, if you want to add a WordCountVisitor, you don't touch the Paragraph or Image structs at all. You just implement the Visitor trait in a new file. You've effectively decoupled the algorithm from the data structure.
The Cost of Double Dispatch
I should be honest with you: this isn't a free lunch. The Visitor pattern uses "double dispatch." First, you call accept() (first dispatch), and then accept() calls back into the visitor's specific method (second dispatch). In Rust, using &mut dyn Visitor means you're paying for dynamic dispatch (vtable lookups) twice.
For a small UI tree, you'll never notice the performance hit. But if you're traversing a million-node AST in a compiler, this overhead adds up. In those cases, I'd suggest sticking to enums and match statements if you can control the types. But for extensibility and clean architecture in larger systems, the Visitor pattern is the gold standard for a reason. It keeps your data "dumb" and your logic "smart," which is exactly how you want it when a project starts to scale.
📋 Practical Task
Implementing a Markdown Statistics Collector
You are building a document analysis tool. You have an existing AST consisting of Heading, Text, and List elements. Your task is to implement a DocumentStatsVisitor that traverses these elements to calculate the total character count and the total number of headings.
Requirements:
- Create a
Visitortrait with methods forvisit_heading,visit_text, andvisit_list. - Implement an
Elementtrait with anacceptmethod. - Create the three concrete structs (
Heading,Text,List) and implementElementfor each. - Implement the
DocumentStatsVisitorstruct and its correspondingVisitortrait implementation. The visitor should maintain internal state (counters) that are updated as it visits elements. - In your
mainfunction, create aVec<Box<dyn Element>>representing a document and pass it through your visitor to print the final statistics.
There are no comments for now.