Visitor Pattern with Rust
Crafting Interpreters makes use of the visitor pattern, which I’m not yet familiar with. To better understand it, I’ve attempted to implement the C# and Java examples from the Wikipedia Visitor pattern article in Rust. Short, isolated examples like these help us grasp the underlying theory more effectively.
We won’t be discussing the theory of the visitor pattern in this post.
🦀 Index of the Complete Series.
![]() |
---|
Visitor Pattern with Rust |
For each of the C# and Java examples, we will first present an equivalent
Rust implementation using enum
, followed by an alternative approach using
trait
s. We will also extend the original examples by introducing additional
components to demonstrate that our Rust code behaves as intended.
❶ Implementing the C# Example in Rust
⓵ Rust enum
Implementation
In this implementation, we use an enum
to represent each of the expression
types: Literal
and Addition
. The complete code is listed below:
// Define enum
pub enum Expression {
Literal(Literal),
Addition(Addition)
}
// Visitor Trait
pub trait Visitor<T> {
fn visit_literal_expression(&self, expression: &Expression) -> T;
fn visit_addition_expression(&self, expression: &Expression) -> T;
}
pub struct Literal {
value: f32,
}
impl Literal {
pub fn new(value: f32) -> Self {
Literal { value }
}
pub fn get_value(&self) -> f32 {
self.value
}
}
pub struct Addition {
pub left: Box<Expression>,
pub right: Box<Expression>,
}
impl Addition {
pub fn new(left: Expression, right: Expression) -> Self {
Addition {
left: Box::new(left),
right: Box::new(right),
}
}
pub fn get_value(&self) -> f32 {
self.left.get_value() + self.right.get_value()
}
}
// Implement accept() for Expression
impl Expression {
pub fn accept<T>(&self, visitor: &dyn Visitor<T>) -> T {
match self {
Expression::Literal(_) => visitor.visit_literal_expression(self),
Expression::Addition(_) => visitor.visit_addition_expression(self),
}
}
pub fn get_value(&self) -> f32 {
match self {
Expression::Literal(lit) => lit.get_value(),
Expression::Addition(add) => add.get_value(),
}
}
}
pub struct ExpressionPrintingVisitor;
impl Visitor<()> for ExpressionPrintingVisitor {
fn visit_literal_expression(&self, expression: &Expression) -> () {
if let Expression::Literal(lit) = expression {
println!("{}", lit.get_value());
}
}
fn visit_addition_expression(&self, expression: &Expression) -> () {
if let Expression::Addition(add) = expression {
let left_val = add.left.get_value();
let right_val = add.right.get_value();
let sum = add.get_value();
println!("{} + {} = {}", left_val, right_val, sum);
}
}
}
fn main() {
let visitor = ExpressionPrintingVisitor;
// Emulate 1 + 2 + 3
let expr = Expression::Addition(Addition::new(
Expression::Addition(Addition::new(
Expression::Literal(Literal::new(1.0)),
Expression::Literal(Literal::new(2.0)),
)),
Expression::Literal(Literal::new(3.0)),
));
expr.accept(&visitor);
}
The output is 3 + 3 = 6
as expected. You can run this code in
Rust Playground to see the result.
🦀 To emulate the following expression: (1 + 2) + (3 + 10)
:
fn main() {
let visitor = ExpressionPrintingVisitor;
// (1 + 2) + (3 + 10)
let expr = Expression::Addition(Addition::new(
Expression::Addition(Addition::new(
Expression::Literal(Literal::new(1.0)),
Expression::Literal(Literal::new(2.0)),
)),
Expression::Addition(Addition::new(
Expression::Literal(Literal::new(3.0)),
Expression::Literal(Literal::new(10.0)),
)),
));
expr.accept(&visitor);
}
Run it in
Rust Playground to see the output 3 + 13 = 16
.
⓶ Rust trait
Implementation
We can't simply turn the Expression
enum into a trait
and use it as shown previously, since the
accept
method involves generic type parameters. As a result,
Expression
cannot be used as a trait object. For example:
pub struct Addition {
pub left: Box<dyn Expression>,
pub right: Box<dyn Expression>,
}
would result in E0038. Instead, we define another trait
that owns the accept
method. The complete code is listed below:
pub trait ExpressionVisitor {
fn visit_literal(&self, literal: &Literal);
fn visit_addition(&self, addition: &Addition);
}
pub trait ExpressionBase {
fn accept(&self, visitor: &dyn ExpressionVisitor);
fn get_value(&self) -> f32;
}
pub struct Literal {
value: f32,
}
impl Literal {
pub fn new(value: f32) -> Self {
Literal { value }
}
pub fn get_value(&self) -> f32 {
self.value
}
}
impl ExpressionBase for Literal {
fn accept(&self, visitor: &dyn ExpressionVisitor) {
visitor.visit_literal(self);
}
fn get_value(&self) -> f32 {
self.get_value()
}
}
pub struct Addition {
left: Box<dyn ExpressionBase>,
right: Box<dyn ExpressionBase>,
}
impl Addition {
pub fn new(left: Box<dyn ExpressionBase>, right: Box<dyn ExpressionBase>) -> Self {
Addition { left, right }
}
pub fn get_value(&self) -> f32 {
self.left.get_value() + self.right.get_value()
}
}
impl ExpressionBase for Addition {
fn accept(&self, visitor: &dyn ExpressionVisitor) {
visitor.visit_addition(self);
}
fn get_value(&self) -> f32 {
self.get_value()
}
}
pub struct ExpressionPrintingVisitor;
impl ExpressionVisitor for ExpressionPrintingVisitor {
fn visit_literal(&self, literal: &Literal) {
println!("{}", literal.get_value());
}
fn visit_addition(&self, addition: &Addition) {
let left = addition.left.get_value();
let right = addition.right.get_value();
let sum = addition.get_value();
println!("{} + {} = {}", left, right, sum);
}
}
fn main() {
let visitor = ExpressionPrintingVisitor;
// Emulate 1 + 2 + 3
let expr: Box<dyn ExpressionBase> = Box::new(Addition::new(
Box::new(Addition::new(
Box::new(Literal::new(1.0)),
Box::new(Literal::new(2.0)),
)),
Box::new(Literal::new(3.0)),
));
expr.accept(&visitor);
// Emulate (1 + 2) + (3 + 10)
let expr: Box<dyn ExpressionBase> = Box::new(Addition::new(
Box::new(Addition::new(
Box::new(Literal::new(1.0)),
Box::new(Literal::new(2.0)),
)),
Box::new(Addition::new(
Box::new(Literal::new(3.0)),
Box::new(Literal::new(10.0)),
)),
));
expr.accept(&visitor);
}
Run it in Rust Playground. The expected output is:
3 + 3 = 6
3 + 13 = 16
⓷ Rust trait
Implementation with a Subtraction
Expression
The Subtraction
expression type closely resembles Addition
.
The complete code is below:
pub trait ExpressionVisitor {
fn visit_literal(&self, literal: &Literal);
fn visit_addition(&self, addition: &Addition);
fn visit_subtraction(&self, subtraction: &Subtraction);
}
pub trait ExpressionBase {
fn accept(&self, visitor: &dyn ExpressionVisitor);
fn get_value(&self) -> f32;
}
pub struct Literal {
value: f32,
}
impl Literal {
pub fn new(value: f32) -> Self {
Literal { value }
}
pub fn get_value(&self) -> f32 {
self.value
}
}
impl ExpressionBase for Literal {
fn accept(&self, visitor: &dyn ExpressionVisitor) {
visitor.visit_literal(self);
}
fn get_value(&self) -> f32 {
self.get_value()
}
}
pub struct Addition {
left: Box<dyn ExpressionBase>,
right: Box<dyn ExpressionBase>,
}
impl Addition {
pub fn new(left: Box<dyn ExpressionBase>, right: Box<dyn ExpressionBase>) -> Self {
Addition { left, right }
}
pub fn get_value(&self) -> f32 {
self.left.get_value() + self.right.get_value()
}
}
impl ExpressionBase for Addition {
fn accept(&self, visitor: &dyn ExpressionVisitor) {
visitor.visit_addition(self);
}
fn get_value(&self) -> f32 {
self.get_value()
}
}
pub struct Subtraction {
left: Box<dyn ExpressionBase>,
right: Box<dyn ExpressionBase>,
}
impl Subtraction {
pub fn new(left: Box<dyn ExpressionBase>, right: Box<dyn ExpressionBase>) -> Self {
Subtraction { left, right }
}
pub fn get_value(&self) -> f32 {
self.left.get_value() - self.right.get_value()
}
}
impl ExpressionBase for Subtraction {
fn accept(&self, visitor: &dyn ExpressionVisitor) {
visitor.visit_subtraction(self);
}
fn get_value(&self) -> f32 {
self.get_value()
}
}
pub struct ExpressionPrintingVisitor;
impl ExpressionVisitor for ExpressionPrintingVisitor {
fn visit_literal(&self, literal: &Literal) {
println!("{}", literal.get_value());
}
fn visit_addition(&self, addition: &Addition) {
let left = addition.left.get_value();
let right = addition.right.get_value();
let sum = addition.get_value();
println!("{} + {} = {}", left, right, sum);
}
fn visit_subtraction(&self, subtraction: &Subtraction) {
let left = subtraction.left.get_value();
let right = subtraction.right.get_value();
let difference = subtraction.get_value();
println!("{} - {} = {}", left, right, difference);
}
}
fn main() {
let visitor = ExpressionPrintingVisitor;
// Emulate (1 + 2) + 3
let expr: Box<dyn ExpressionBase> = Box::new(Addition::new(
Box::new(Addition::new(
Box::new(Literal::new(1.0)),
Box::new(Literal::new(2.0)),
)),
Box::new(Literal::new(3.0)),
));
expr.accept(&visitor);
// Emulate 1 - 2 = -1
let expr: Box<dyn ExpressionBase> = Box::new(Subtraction::new(
Box::new(Literal::new(1.0)),
Box::new(Literal::new(2.0))
));
expr.accept(&visitor);
// Emulate (1 - 2) + 8 = 7
let expr: Box<dyn ExpressionBase> = Box::new(Addition::new(
Box::new(Subtraction::new(
Box::new(Literal::new(1.0)),
Box::new(Literal::new(2.0))
)),
Box::new(Literal::new(8.0))
));
expr.accept(&visitor);
}
Running it in Rust Playground gives this output:
3 + 3 = 6
1 - 2 = -1
-1 + 8 = 7
❷ Implementing the Java Example in Rust
⓵ Rust enum
Implementation
In this version, we treat Car
as just another CarElement
and visit it like any other part. The complete code is listed below:
#[derive(Clone)]
pub struct Wheel {
name: String,
}
impl Wheel {
pub fn new(name: &str) -> Self {
Wheel { name: name.to_string() }
}
pub fn get_name(&self) -> &str {
self.name.as_str()
}
}
#[derive(Clone)]
pub struct Body;
#[derive(Clone)]
pub struct Engine;
#[derive(Clone)]
pub struct Car {
elements: Vec<Box<CarElement>>,
}
impl Car {
pub fn new() -> Self {
let mut car = Car {
elements: vec![
Box::new(CarElement::Wheel(Wheel::new("front left"))),
Box::new(CarElement::Wheel(Wheel::new("front right"))),
Box::new(CarElement::Wheel(Wheel::new("back left"))),
Box::new(CarElement::Wheel(Wheel::new("back right"))),
Box::new(CarElement::Body(Body {})),
Box::new(CarElement::Engine(Engine {})),
]
};
// Add the car itself as a CarElement
car.elements.push(Box::new(CarElement::Car(car.clone())));
car
}
pub fn accept(&self, visitor: &dyn CarElementVisitor) {
for element in &self.elements {
element.accept(visitor);
}
visitor.visit_car(self);
}
pub fn get_elements(&self) -> &Vec<Box<CarElement>> {
&self.elements
}
}
// Define enum.
#[derive(Clone)]
pub enum CarElement {
Wheel(Wheel),
Body(Body),
Engine(Engine),
Car(Car),
}
// CarElementVisitor Trait
pub trait CarElementVisitor {
fn visit_body(&self, body: &Body);
fn visit_car(&self, car: &Car);
fn visit_engine(&self, engine: &Engine);
fn visit_wheel(&self, wheel: &Wheel);
}
// Implement accept() for CarElement
impl CarElement {
pub fn accept(&self, visitor: &dyn CarElementVisitor) {
match self {
CarElement::Body(body) => visitor.visit_body(body),
CarElement::Car(car) => visitor.visit_car(car),
CarElement::Engine(engine) => visitor.visit_engine(engine),
CarElement::Wheel(wheel) => visitor.visit_wheel(wheel),
}
}
}
pub struct CarElementDoVisitor;
impl CarElementVisitor for CarElementDoVisitor {
fn visit_body(&self, _: &Body) {
println!("Moving my body");
}
fn visit_car(&self, _: &Car) {
println!("Starting my car");
}
fn visit_engine(&self, _: &Engine) {
println!("Starting my engine");
}
fn visit_wheel(&self, wheel: &Wheel) {
println!("Kicking my {} wheel", wheel.get_name());
}
}
pub struct CarElementPrintVisitor;
impl CarElementVisitor for CarElementPrintVisitor {
fn visit_body(&self, _: &Body) {
println!("Visiting body");
}
fn visit_car(&self, _: &Car) {
println!("Visiting car");
}
fn visit_engine(&self, _: &Engine) {
println!("Visiting engine");
}
fn visit_wheel(&self, wheel: &Wheel) {
println!("Visiting {} wheel", wheel.get_name());
}
}
fn main() {
let print_visitor = CarElementPrintVisitor;
let do_visitor = CarElementDoVisitor;
let car = Car::new();
for element in car.get_elements() {
element.accept(&print_visitor);
}
for element in car.get_elements() {
element.accept(&do_visitor);
}
}
Run it in Rust Playground and observe that the output matches the Java example on Wikipedia:
Visiting front left wheel
Visiting front right wheel
Visiting back left wheel
Visiting back right wheel
Visiting body
Visiting engine
Visiting car
Kicking my front left wheel
Kicking my front right wheel
Kicking my back left wheel
Kicking my back right wheel
Moving my body
Starting my engine
Starting my car
⓶ Rust trait
Implementation
Since the accept
method doesn't have generic parameters, the trait-based
implementation mirrors the earlier enum version. The complete code:
pub trait CarElementVisitor {
fn visit_wheel(&self, wheel: &Wheel);
fn visit_body(&self, body: &Body);
fn visit_engine(&self, engine: &Engine);
fn visit_car(&self, car: &Car);
}
pub trait CarElement {
fn accept(&self, visitor: &dyn CarElementVisitor);
}
pub struct Wheel {
name: String,
}
impl Wheel {
pub fn new(name: &str) -> Self {
Wheel { name: name.to_string() }
}
pub fn get_name(&self) -> &str {
&self.name
}
}
impl CarElement for Wheel {
fn accept(&self, visitor: &dyn CarElementVisitor) {
visitor.visit_wheel(self);
}
}
pub struct Body;
impl CarElement for Body {
fn accept(&self, visitor: &dyn CarElementVisitor) {
visitor.visit_body(self);
}
}
pub struct Engine;
impl CarElement for Engine {
fn accept(&self, visitor: &dyn CarElementVisitor) {
visitor.visit_engine(self);
}
}
pub struct Car {
elements: Vec<Box<dyn CarElement>>,
}
impl Car {
pub fn new() -> Self {
Car {
elements: vec![
Box::new(Wheel::new("front left")),
Box::new(Wheel::new("front right")),
Box::new(Wheel::new("back left")),
Box::new(Wheel::new("back right")),
Box::new(Body),
Box::new(Engine),
],
}
}
}
impl CarElement for Car {
fn accept(&self, visitor: &dyn CarElementVisitor) {
for element in &self.elements {
element.accept(visitor);
}
visitor.visit_car(self);
}
}
pub struct CarElementPrintVisitor;
impl CarElementVisitor for CarElementPrintVisitor {
fn visit_wheel(&self, wheel: &Wheel) {
println!("Visiting {} wheel", wheel.get_name());
}
fn visit_body(&self, _: &Body) {
println!("Visiting body");
}
fn visit_engine(&self, _: &Engine) {
println!("Visiting engine");
}
fn visit_car(&self, _: &Car) {
println!("Visiting car");
}
}
pub struct CarElementDoVisitor;
impl CarElementVisitor for CarElementDoVisitor {
fn visit_body(&self, _: &Body) {
println!("Moving my body");
}
fn visit_car(&self, _: &Car) {
println!("Starting my car");
}
fn visit_engine(&self, _: &Engine) {
println!("Starting my engine");
}
fn visit_wheel(&self, wheel: &Wheel) {
println!("Kicking my {} wheel", wheel.get_name());
}
}
fn main() {
let car = Car::new();
let print_visitor = CarElementPrintVisitor;
let do_visitor = CarElementDoVisitor;
car.accept(&print_visitor);
car.accept(&do_visitor);
}
The output should be identical—see it in Rust Playground.
⓷ Rust trait
Implementation with a Dashboard
Element
For illustration, Dashboard
will contain five fields. We also implement the
Display trait to make the visit output more idiomatic.
Dashboard
is now a component of Car
, so instantiating
a car includes a dashboard. Full code:
use std::fmt;
pub trait CarElementVisitor {
fn visit_wheel(&self, wheel: &Wheel);
fn visit_body(&self, body: &Body);
fn visit_engine(&self, engine: &Engine);
fn visit_car(&self, car: &Car);
fn visit_dashboard(&self, dashboard: &Dashboard);
}
pub trait CarElement {
fn accept(&self, visitor: &dyn CarElementVisitor);
}
pub struct Wheel {
name: String,
}
impl Wheel {
pub fn new(name: &str) -> Self {
Wheel { name: name.to_string() }
}
pub fn get_name(&self) -> &str {
&self.name
}
}
impl CarElement for Wheel {
fn accept(&self, visitor: &dyn CarElementVisitor) {
visitor.visit_wheel(self);
}
}
pub struct Body;
impl CarElement for Body {
fn accept(&self, visitor: &dyn CarElementVisitor) {
visitor.visit_body(self);
}
}
pub struct Engine;
impl CarElement for Engine {
fn accept(&self, visitor: &dyn CarElementVisitor) {
visitor.visit_engine(self);
}
}
pub struct Dashboard {
screen_size_in_inches: f32,
has_navigation: bool,
display_type: String,
num_buttons: u8,
language: Option<String>,
}
impl fmt::Display for Dashboard {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let language = self.language.as_deref().unwrap_or("not specified");
write!(f,
"Screen Size: {}\", GPS: {}, Display Type: {}, Number of Buttons: {}, Language: {}",
self.screen_size_in_inches,
self.has_navigation,
self.display_type,
self.num_buttons,
language
)
}
}
impl Dashboard {
pub fn new(screen_size_in_inches: f32,
has_navigation: bool,
display_type: &str,
num_buttons: u8,
language: Option<String>,
) -> Self {
Dashboard {
screen_size_in_inches,
has_navigation,
display_type: display_type.to_string(),
num_buttons,
language,
}
}
}
impl CarElement for Dashboard {
fn accept(&self, visitor: &dyn CarElementVisitor) {
visitor.visit_dashboard(self);
}
}
pub struct Car {
elements: Vec<Box<dyn CarElement>>,
}
impl Car {
pub fn new() -> Self {
Car {
elements: vec![
Box::new(Wheel::new("front left")),
Box::new(Wheel::new("front right")),
Box::new(Wheel::new("back left")),
Box::new(Wheel::new("back right")),
Box::new(Body),
Box::new(Engine),
Box::new(Dashboard::new(12.00,
false, "OLED", 6, Some(String::from("English")))),
],
}
}
}
impl CarElement for Car {
fn accept(&self, visitor: &dyn CarElementVisitor) {
for element in &self.elements {
element.accept(visitor);
}
visitor.visit_car(self);
}
}
pub struct CarElementPrintVisitor;
impl CarElementVisitor for CarElementPrintVisitor {
fn visit_wheel(&self, wheel: &Wheel) {
println!("Visiting {} wheel", wheel.get_name());
}
fn visit_body(&self, _: &Body) {
println!("Visiting body");
}
fn visit_engine(&self, _: &Engine) {
println!("Visiting engine");
}
fn visit_car(&self, _: &Car) {
println!("Visiting car");
}
fn visit_dashboard(&self, dashboard: &Dashboard) {
println!("Visiting dashboard: {}", dashboard);
}
}
pub struct CarElementDoVisitor;
impl CarElementVisitor for CarElementDoVisitor {
fn visit_body(&self, _: &Body) {
println!("Moving my body");
}
fn visit_car(&self, _: &Car) {
println!("Starting my car");
}
fn visit_engine(&self, _: &Engine) {
println!("Starting my engine");
}
fn visit_wheel(&self, wheel: &Wheel) {
println!("Kicking my {} wheel", wheel.get_name());
}
fn visit_dashboard(&self, dashboard: &Dashboard) {
println!("Navigating using dashboard: {}", dashboard);
}
}
fn main() {
let car = Car::new();
let print_visitor = CarElementPrintVisitor;
let do_visitor = CarElementDoVisitor;
car.accept(&print_visitor);
car.accept(&do_visitor);
}
Run it in Rust Playground. You should see:
Visiting front left wheel
Visiting front right wheel
Visiting back left wheel
Visiting back right wheel
Visiting body
Visiting engine
Visiting dashboard: Screen Size: 12", GPS: false, Display Type: OLED, Number of Buttons: 6, Language: English
Visiting car
Kicking my front left wheel
Kicking my front right wheel
Kicking my back left wheel
Kicking my back right wheel
Moving my body
Starting my engine
Navigating using dashboard: Screen Size: 12", GPS: false, Display Type: OLED, Number of Buttons: 6, Language: English
Starting my car
❸ As stated at the outset, this post documents my personal journey toward understanding the visitor pattern. It is not meant to be a tutorial.
Thank you for reading. I hope you find this post helpful. Stay safe, as always.
✿✿✿
Feature image sources:
- https://www.omgubuntu.co.uk/2024/03/ubuntu-24-04-wallpaper
- https://in.pinterest.com/pin/337277459600111737/
- https://www.rust-lang.org/
- https://www.wannapik.com/vectors/21725