第12章: Visitor パターン¶
はじめに¶
Visitor パターンは、データ構造の要素に対する操作を、データ構造から分離するパターンです。関数型プログラミングでは、パターンマッチングを使ってこのパターンを自然に実現できます。
本章では、式の評価、AST 変換、ドキュメント処理など、様々な例を通じて Visitor パターンの Rust での実装を学びます。
1. パターンの構造¶
2. 式の Visitor¶
式の定義¶
/// 数式 AST
#[derive(Debug, Clone, PartialEq)]
pub enum Expr {
Number(f64),
Variable(String),
Add(Box<Expr>, Box<Expr>),
Subtract(Box<Expr>, Box<Expr>),
Multiply(Box<Expr>, Box<Expr>),
Divide(Box<Expr>, Box<Expr>),
}
impl Expr {
pub fn number(n: f64) -> Expr {
Expr::Number(n)
}
pub fn add(left: Expr, right: Expr) -> Expr {
Expr::Add(Box::new(left), Box::new(right))
}
pub fn multiply(left: Expr, right: Expr) -> Expr {
Expr::Multiply(Box::new(left), Box::new(right))
}
}
Visitor トレイト¶
/// Visitor トレイト
pub trait ExprVisitor<R> {
fn visit_number(&mut self, n: f64) -> R;
fn visit_variable(&mut self, name: &str) -> R;
fn visit_add(&mut self, left: R, right: R) -> R;
fn visit_subtract(&mut self, left: R, right: R) -> R;
fn visit_multiply(&mut self, left: R, right: R) -> R;
fn visit_divide(&mut self, left: R, right: R) -> R;
}
/// 式を訪問
pub fn visit<R>(expr: &Expr, visitor: &mut dyn ExprVisitor<R>) -> R {
match expr {
Expr::Number(n) => visitor.visit_number(*n),
Expr::Variable(name) => visitor.visit_variable(name),
Expr::Add(left, right) => {
let l = visit(left, visitor);
let r = visit(right, visitor);
visitor.visit_add(l, r)
}
Expr::Subtract(left, right) => {
let l = visit(left, visitor);
let r = visit(right, visitor);
visitor.visit_subtract(l, r)
}
Expr::Multiply(left, right) => {
let l = visit(left, visitor);
let r = visit(right, visitor);
visitor.visit_multiply(l, r)
}
Expr::Divide(left, right) => {
let l = visit(left, visitor);
let r = visit(right, visitor);
visitor.visit_divide(l, r)
}
}
}
評価 Visitor¶
/// 評価 Visitor
pub struct Evaluator {
pub variables: HashMap<String, f64>,
}
impl ExprVisitor<f64> for Evaluator {
fn visit_number(&mut self, n: f64) -> f64 {
n
}
fn visit_variable(&mut self, name: &str) -> f64 {
self.variables.get(name).copied().unwrap_or(0.0)
}
fn visit_add(&mut self, left: f64, right: f64) -> f64 {
left + right
}
fn visit_subtract(&mut self, left: f64, right: f64) -> f64 {
left - right
}
fn visit_multiply(&mut self, left: f64, right: f64) -> f64 {
left * right
}
fn visit_divide(&mut self, left: f64, right: f64) -> f64 {
if right != 0.0 { left / right } else { f64::NAN }
}
}
出力 Visitor¶
/// 式を文字列に変換する Visitor
pub struct Printer;
impl ExprVisitor<String> for Printer {
fn visit_number(&mut self, n: f64) -> String {
n.to_string()
}
fn visit_variable(&mut self, name: &str) -> String {
name.to_string()
}
fn visit_add(&mut self, left: String, right: String) -> String {
format!("({} + {})", left, right)
}
fn visit_subtract(&mut self, left: String, right: String) -> String {
format!("({} - {})", left, right)
}
fn visit_multiply(&mut self, left: String, right: String) -> String {
format!("({} * {})", left, right)
}
fn visit_divide(&mut self, left: String, right: String) -> String {
format!("({} / {})", left, right)
}
}
使用例¶
// (2 + 3) * 4
let expr = Expr::multiply(
Expr::add(Expr::number(2.0), Expr::number(3.0)),
Expr::number(4.0),
);
// 評価
let mut evaluator = Evaluator { variables: HashMap::new() };
let result = visit(&expr, &mut evaluator);
assert_eq!(result, 20.0);
// 文字列変換
let mut printer = Printer;
let string = visit(&expr, &mut printer);
assert_eq!(string, "((2 + 3) * 4)");
3. 関数型 Visitor¶
パターンマッチングを使った直接的な実装:
/// 関数型評価
pub fn evaluate(expr: &Expr, vars: &HashMap<String, f64>) -> f64 {
match expr {
Expr::Number(n) => *n,
Expr::Variable(name) => vars.get(name).copied().unwrap_or(0.0),
Expr::Add(l, r) => evaluate(l, vars) + evaluate(r, vars),
Expr::Subtract(l, r) => evaluate(l, vars) - evaluate(r, vars),
Expr::Multiply(l, r) => evaluate(l, vars) * evaluate(r, vars),
Expr::Divide(l, r) => {
let r_val = evaluate(r, vars);
if r_val != 0.0 {
evaluate(l, vars) / r_val
} else {
f64::NAN
}
}
}
}
/// 変数を収集
pub fn collect_variables(expr: &Expr) -> HashSet<String> {
match expr {
Expr::Number(_) => HashSet::new(),
Expr::Variable(name) => {
let mut set = HashSet::new();
set.insert(name.clone());
set
}
Expr::Add(l, r) | Expr::Subtract(l, r) | Expr::Multiply(l, r) | Expr::Divide(l, r) => {
let mut vars = collect_variables(l);
vars.extend(collect_variables(r));
vars
}
}
}
/// 式を簡略化
pub fn simplify(expr: &Expr) -> Expr {
match expr {
Expr::Add(l, r) => {
let l = simplify(l);
let r = simplify(r);
match (&l, &r) {
(Expr::Number(0.0), _) => r,
(_, Expr::Number(0.0)) => l,
(Expr::Number(a), Expr::Number(b)) => Expr::Number(a + b),
_ => Expr::Add(Box::new(l), Box::new(r)),
}
}
Expr::Multiply(l, r) => {
let l = simplify(l);
let r = simplify(r);
match (&l, &r) {
(Expr::Number(0.0), _) | (_, Expr::Number(0.0)) => Expr::Number(0.0),
(Expr::Number(1.0), _) => r,
(_, Expr::Number(1.0)) => l,
(Expr::Number(a), Expr::Number(b)) => Expr::Number(a * b),
_ => Expr::Multiply(Box::new(l), Box::new(r)),
}
}
_ => expr.clone(),
}
}
4. ドキュメント Visitor¶
/// ドキュメント要素
#[derive(Debug, Clone)]
pub enum DocElement {
Text(String),
Bold(Box<DocElement>),
Italic(Box<DocElement>),
Link { text: Box<DocElement>, url: String },
Paragraph(Vec<DocElement>),
Document(Vec<DocElement>),
}
/// HTML 変換
pub fn to_html(element: &DocElement) -> String {
match element {
DocElement::Text(text) => text.clone(),
DocElement::Bold(inner) => format!("<strong>{}</strong>", to_html(inner)),
DocElement::Italic(inner) => format!("<em>{}</em>", to_html(inner)),
DocElement::Link { text, url } => {
format!(r#"<a href="{}">{}</a>"#, url, to_html(text))
}
DocElement::Paragraph(elements) => {
let content: String = elements.iter().map(to_html).collect();
format!("<p>{}</p>", content)
}
DocElement::Document(elements) => {
elements.iter().map(to_html).collect()
}
}
}
/// Markdown 変換
pub fn to_markdown(element: &DocElement) -> String {
match element {
DocElement::Text(text) => text.clone(),
DocElement::Bold(inner) => format!("**{}**", to_markdown(inner)),
DocElement::Italic(inner) => format!("*{}*", to_markdown(inner)),
DocElement::Link { text, url } => {
format!("[{}]({})", to_markdown(text), url)
}
DocElement::Paragraph(elements) => {
let content: String = elements.iter().map(to_markdown).collect();
format!("{}\n\n", content)
}
DocElement::Document(elements) => {
elements.iter().map(to_markdown).collect()
}
}
}
5. パターンの利点¶
- 分離: データ構造と操作を分離
- 拡張性: 新しい操作を追加しやすい
- 単一責任: 各 Visitor は単一の操作に集中
- 型安全性: コンパイル時に型チェック
6. Rust での特徴¶
パターンマッチングによる網羅性¶
// 全てのケースをカバーしないとコンパイルエラー
match expr {
Expr::Number(n) => { /* ... */ }
Expr::Variable(name) => { /* ... */ }
Expr::Add(l, r) => { /* ... */ }
// 他のケースを忘れると警告
}
enum による代数的データ型¶
// 直和型として自然に表現
pub enum Expr {
Number(f64),
Add(Box<Expr>, Box<Expr>),
// ...
}
Scala との比較¶
| 概念 | Scala | Rust |
|---|---|---|
| データ定義 | sealed trait + case class | enum |
| パターンマッチ | match | match |
| 再帰型 | 直接参照 | Box で包む |
| Visitor | trait | trait |
まとめ¶
本章では、Visitor パターンについて学びました:
- 式の Visitor: 評価、出力、変数収集
- 関数型アプローチ: パターンマッチングによる直接実装
- AST 変換: 式の簡略化
- ドキュメント Visitor: HTML/Markdown 変換
Rust の enum とパターンマッチングを使うことで、Visitor パターンを型安全かつ簡潔に実装できます。
参考コード¶
本章のコード例は以下のファイルで確認できます:
- ソースコード:
apps/rust/part4/src/chapter12.rs
次章予告¶
次章では、Abstract Factory パターンについて学びます。関連するオブジェクト群を生成する方法を探ります。