Rust — visitor vs enum Link to heading
A visitor pattern is to add an action for a group of classes, without modifying each of the class itself. For simplicity, let’s say these…
trait Data {
fn call<A: Action>(&self, action: &A) -> A::Result;
}
struct X {
// ...
}
impl Data for X {
fn call<A: Action>(&self, action: &A) -> A::Result {
action.act_x(self)
}
}
struct Y {
// ...
}
impl Data for Y {
fn call<A: Action>(&self, action: &A) -> A::Result {
action.act_y(self)
}
}
trait Action {
type Result;
fn act_x(&self, x: &X) -> Self::Result;
fn act_y(&self, y: &Y) -> Self::Result;
}
In order to add an action, we create a struct that implements Action:
struct Action1;
impl Action for Action1 {
type Result = i32;
fn act_x(&self, x: &X) -> Self::Result { 1 }
fn act_y(&self, x: &Y) -> Self::Result { 2 }
}
struct Action2;
impl Action for Action2 {
type Result = &'static str;
fn act_x(&self, x: &X) -> Self::Result { "x" }
fn act_y(&self, x: &Y) -> Self::Result { "y" }
}
// can add as many actions as needed
// ...
Now that we have Data = {X, Y} and Action = {Action1, Action2}, we can have total |Data| X |Action| possible combinations.
let x = X{ /* ... */ };
let y = Y{ /* ... */ };
let a1 = Action1;
let a2 = Action2;
// {X, Y} x {Action1, Action2}
let result_x1 = x.call(&a1);
let result_x2 = x.call(&a2);
let result_y1 = y.call(&a1);
let result_y2 = y.call(&a2);
This way, we can add arbitrary many actions to the existing group of classes without modifying the classes themselves. The downside is that it makes the code verbose. What is the alternative to the visitor pattern?
For Rust, we can use pattern-matching with enum:
enum Data {
X(X),
Y(Y),
}
struct X {
// fill in
}
struct Y {
// fill in
}
fn action1(data: &Data) -> i32 {
match data {
Data::X(x) => 1,
Data::Y(y) => 2,
}
}
fn action2(data: &Data) -> &'static str {
match data {
Data::X(x) => "x",
Data::Y(y) => "y",
}
}
fn perform_all_combinations() {
let x = Data::X(X{ /* ... */ });
let y = Data::Y(Y{ /* ... */ });
let x1 = action1(&x);
let x2 = action2(&x);
let y1 = action1(&y);
let y2 = action2(&y);
}
This simplifies the code greatly and removes indirection. Does this mean we should always prefer enum pattern match over visitor pattern?
As typically is the case, there is no free lunch. One drawback of enum pattern match is that by creating an enum, the object size of Data will be set to the largest size among its variants, so there can be waste in the memory space. One can mitigate this problem by wrapping X and Y into Box, but that itself introduces yet another indirection with additional overhead for dynamic memory allocation.
However, a typical use case of visitor pattern probably requires the data type to be dynamically determined at run time anyways, so the visitor pattern will also likely require Box overhead as well.
In conclusion, the two patterns let us achieve the same thing, and I don’t think one is better than another in all aspects. Rather, it probably comes down to personal preference in how the code is organized.