Visitor
説明
ビジターは、異種混在のオブジェクトのコレクションに対して動作するアルゴリズムをカプセル化します。これにより、データ(またはその主要な振る舞い)を変更することなく、同じデータに対して複数の異なるアルゴリズムを記述できます。
さらに、Visitor パターンを使うと、オブジェクトのコレクションの走査と、各オブジェクトに対して実行される操作を分離できます。
例
// 訪問するデータ
mod ast {
pub enum Stmt {
Expr(Expr),
Let(Name, Expr),
}
pub struct Name {
value: String,
}
pub enum Expr {
IntLit(i64),
Add(Box<Expr>, Box<Expr>),
Sub(Box<Expr>, Box<Expr>),
}
}
// 抽象ビジター
mod visit {
use ast::*;
pub trait Visitor<T> {
fn visit_name(&mut self, n: &Name) -> T;
fn visit_stmt(&mut self, s: &Stmt) -> T;
fn visit_expr(&mut self, e: &Expr) -> T;
}
}
use ast::*;
use visit::*;
// 具体的な実装例 - AST をコードとして解釈しながら走査する。
struct Interpreter;
impl Visitor<i64> for Interpreter {
fn visit_name(&mut self, n: &Name) -> i64 {
panic!()
}
fn visit_stmt(&mut self, s: &Stmt) -> i64 {
match *s {
Stmt::Expr(ref e) => self.visit_expr(e),
Stmt::Let(..) => unimplemented!(),
}
}
fn visit_expr(&mut self, e: &Expr) -> i64 {
match *e {
Expr::IntLit(n) => n,
Expr::Add(ref lhs, ref rhs) => self.visit_expr(lhs) + self.visit_expr(rhs),
Expr::Sub(ref lhs, ref rhs) => self.visit_expr(lhs) - self.visit_expr(rhs),
}
}
}
たとえば型チェッカーなど、さらに別のビジターを実装できます。その際、AST データを変更する必要はありません。
動機
Visitor パターンは、異種混在のデータにアルゴリズムを適用したいあらゆる場面で有用です。データが同種である場合は、イテレーターのようなパターンを使用できます。(関数型のアプローチではなく)ビジターオブジェクトを使用すると、ビジターに状態を持たせることができるため、ノード間で情報をやり取りできます。
議論
visit_* メソッドが、例とは異なり () を返す、つまり値を返さない形になることは一般的です。その場合、走査コードを切り出してアルゴリズム間で共有できます(また、何もしないデフォルトメソッドを提供することもできます)。Rust では、これを行う一般的な方法は、各データに対して walk_* 関数を提供することです。たとえば、
pub fn walk_expr(visitor: &mut Visitor, e: &Expr) {
match *e {
Expr::IntLit(_) => {}
Expr::Add(ref lhs, ref rhs) => {
visitor.visit_expr(lhs);
visitor.visit_expr(rhs);
}
Expr::Sub(ref lhs, ref rhs) => {
visitor.visit_expr(lhs);
visitor.visit_expr(rhs);
}
}
}
他の言語(たとえば Java)では、同じ役割を果たす accept メソッドをデータが持つことが一般的です。
関連項目
Visitor パターンは、ほとんどの OO 言語で一般的なパターンです。
fold パターンは Visitor に似ていますが、訪問したデータ構造の新しいバージョンを生成します。