Visitor Pattern em Rust: Percorrendo Estruturas com Duplo Despacho

Implementação completa do padrão Visitor em Rust com enums, trait-based visitor, duplo despacho, AST walker e exemplo prático de formatador de código.

O Visitor Pattern (Padrão Visitante) permite adicionar novas operações a uma estrutura de objetos sem modificar as classes (ou, em Rust, os tipos) que compõem essa estrutura. Ele resolve o problema do duplo despacho (double dispatch): selecionar o comportamento com base tanto no tipo do visitante quanto no tipo do elemento visitado.

Em Rust, o Visitor Pattern tem duas implementações idiomáticas: a baseada em enum (mais comum e recomendada quando os tipos são conhecidos) e a baseada em traits (para extensibilidade). A versão com enum se beneficia do pattern matching exaustivo, enquanto a versão com traits permite que novos visitantes sejam adicionados sem recompilar a estrutura.

Problema

Considere um compilador com uma AST (Abstract Syntax Tree). A AST tem vários tipos de nós (literais, operações binárias, chamadas de função). Precisamos executar diversas operações sobre a AST — impressão, cálculo, otimização, verificação de tipos — sem modificar os nós da árvore a cada nova operação.

Sem o Visitor Pattern, cada operação exigiria modificar todos os tipos de nó, violando o Princípio Aberto/Fechado.

Solução em Rust

Abordagem 1: Visitor com Enum (Mais Idiomático)

/// Nós da AST representados como enum
#[derive(Debug, Clone)]
enum Expressao {
    /// Literal numérico: 42, 3.14
    Numero(f64),
    /// Literal string: "olá"
    Texto(String),
    /// Literal booleano: true, false
    Booleano(bool),
    /// Operação binária: esquerda op direita
    OperacaoBinaria {
        esquerda: Box<Expressao>,
        operador: Operador,
        direita: Box<Expressao>,
    },
    /// Operação unária: op expressão
    OperacaoUnaria {
        operador: OperadorUnario,
        operando: Box<Expressao>,
    },
    /// Chamada de função: nome(args...)
    ChamadaFuncao {
        nome: String,
        argumentos: Vec<Expressao>,
    },
    /// Condicional: if cond then else
    SeEntao {
        condicao: Box<Expressao>,
        entao: Box<Expressao>,
        senao: Option<Box<Expressao>>,
    },
}

#[derive(Debug, Clone, Copy)]
enum Operador {
    Soma,
    Subtracao,
    Multiplicacao,
    Divisao,
    Igual,
    Maior,
    Menor,
}

#[derive(Debug, Clone, Copy)]
enum OperadorUnario {
    Negacao,      // -x
    NegacaoLogica, // !x
}

/// Trait do Visitor — define uma operação sobre a AST
trait VisitanteExpressao {
    type Resultado;

    fn visitar(&self, expr: &Expressao) -> Self::Resultado;
}

/// Visitor 1: Impressão formatada (pretty print)
struct ImpressorFormatado {
    indentacao: usize,
}

impl ImpressorFormatado {
    fn novo() -> Self {
        ImpressorFormatado { indentacao: 0 }
    }

    fn formatar_operador(&self, op: &Operador) -> &str {
        match op {
            Operador::Soma => "+",
            Operador::Subtracao => "-",
            Operador::Multiplicacao => "*",
            Operador::Divisao => "/",
            Operador::Igual => "==",
            Operador::Maior => ">",
            Operador::Menor => "<",
        }
    }
}

impl VisitanteExpressao for ImpressorFormatado {
    type Resultado = String;

    fn visitar(&self, expr: &Expressao) -> String {
        match expr {
            Expressao::Numero(n) => {
                if *n == (*n as i64) as f64 {
                    format!("{}", *n as i64)
                } else {
                    format!("{}", n)
                }
            }
            Expressao::Texto(s) => format!("\"{}\"", s),
            Expressao::Booleano(b) => format!("{}", b),
            Expressao::OperacaoBinaria { esquerda, operador, direita } => {
                format!(
                    "({} {} {})",
                    self.visitar(esquerda),
                    self.formatar_operador(operador),
                    self.visitar(direita),
                )
            }
            Expressao::OperacaoUnaria { operador, operando } => {
                let simbolo = match operador {
                    OperadorUnario::Negacao => "-",
                    OperadorUnario::NegacaoLogica => "!",
                };
                format!("{}{}", simbolo, self.visitar(operando))
            }
            Expressao::ChamadaFuncao { nome, argumentos } => {
                let args: Vec<String> = argumentos.iter()
                    .map(|a| self.visitar(a))
                    .collect();
                format!("{}({})", nome, args.join(", "))
            }
            Expressao::SeEntao { condicao, entao, senao } => {
                let mut resultado = format!(
                    "if {} {{ {} }}",
                    self.visitar(condicao),
                    self.visitar(entao),
                );
                if let Some(s) = senao {
                    resultado.push_str(&format!(" else {{ {} }}", self.visitar(s)));
                }
                resultado
            }
        }
    }
}

/// Visitor 2: Avaliador (calcula o resultado)
struct Avaliador;

#[derive(Debug, Clone)]
enum Valor {
    Numero(f64),
    Texto(String),
    Booleano(bool),
    Nulo,
}

impl std::fmt::Display for Valor {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Valor::Numero(n) => write!(f, "{}", n),
            Valor::Texto(s) => write!(f, "{}", s),
            Valor::Booleano(b) => write!(f, "{}", b),
            Valor::Nulo => write!(f, "nulo"),
        }
    }
}

impl VisitanteExpressao for Avaliador {
    type Resultado = Result<Valor, String>;

    fn visitar(&self, expr: &Expressao) -> Result<Valor, String> {
        match expr {
            Expressao::Numero(n) => Ok(Valor::Numero(*n)),
            Expressao::Texto(s) => Ok(Valor::Texto(s.clone())),
            Expressao::Booleano(b) => Ok(Valor::Booleano(*b)),
            Expressao::OperacaoBinaria { esquerda, operador, direita } => {
                let esq = self.visitar(esquerda)?;
                let dir = self.visitar(direita)?;

                match (esq, operador, dir) {
                    (Valor::Numero(a), Operador::Soma, Valor::Numero(b)) => {
                        Ok(Valor::Numero(a + b))
                    }
                    (Valor::Numero(a), Operador::Subtracao, Valor::Numero(b)) => {
                        Ok(Valor::Numero(a - b))
                    }
                    (Valor::Numero(a), Operador::Multiplicacao, Valor::Numero(b)) => {
                        Ok(Valor::Numero(a * b))
                    }
                    (Valor::Numero(a), Operador::Divisao, Valor::Numero(b)) => {
                        if b == 0.0 {
                            Err("Divisão por zero".to_string())
                        } else {
                            Ok(Valor::Numero(a / b))
                        }
                    }
                    (Valor::Numero(a), Operador::Maior, Valor::Numero(b)) => {
                        Ok(Valor::Booleano(a > b))
                    }
                    (Valor::Numero(a), Operador::Menor, Valor::Numero(b)) => {
                        Ok(Valor::Booleano(a < b))
                    }
                    (Valor::Texto(a), Operador::Soma, Valor::Texto(b)) => {
                        Ok(Valor::Texto(format!("{}{}", a, b)))
                    }
                    _ => Err("Operação não suportada para estes tipos".to_string()),
                }
            }
            Expressao::OperacaoUnaria { operador, operando } => {
                let val = self.visitar(operando)?;
                match (operador, val) {
                    (OperadorUnario::Negacao, Valor::Numero(n)) => Ok(Valor::Numero(-n)),
                    (OperadorUnario::NegacaoLogica, Valor::Booleano(b)) => {
                        Ok(Valor::Booleano(!b))
                    }
                    _ => Err("Operação unária não suportada".to_string()),
                }
            }
            Expressao::ChamadaFuncao { nome, argumentos } => {
                let args: Result<Vec<Valor>, String> = argumentos.iter()
                    .map(|a| self.visitar(a))
                    .collect();
                let args = args?;

                match nome.as_str() {
                    "max" => {
                        let nums: Result<Vec<f64>, _> = args.iter().map(|v| {
                            if let Valor::Numero(n) = v { Ok(*n) }
                            else { Err("max() aceita apenas números".to_string()) }
                        }).collect();
                        let nums = nums?;
                        Ok(Valor::Numero(nums.into_iter().fold(f64::NEG_INFINITY, f64::max)))
                    }
                    "len" => {
                        if let Some(Valor::Texto(s)) = args.first() {
                            Ok(Valor::Numero(s.len() as f64))
                        } else {
                            Err("len() aceita apenas texto".to_string())
                        }
                    }
                    _ => Err(format!("Função '{}' não definida", nome)),
                }
            }
            Expressao::SeEntao { condicao, entao, senao } => {
                let cond = self.visitar(condicao)?;
                match cond {
                    Valor::Booleano(true) => self.visitar(entao),
                    Valor::Booleano(false) => {
                        if let Some(s) = senao {
                            self.visitar(s)
                        } else {
                            Ok(Valor::Nulo)
                        }
                    }
                    _ => Err("Condição deve ser booleana".to_string()),
                }
            }
        }
    }
}

/// Visitor 3: Contador de nós
struct ContadorNos;

impl VisitanteExpressao for ContadorNos {
    type Resultado = usize;

    fn visitar(&self, expr: &Expressao) -> usize {
        match expr {
            Expressao::Numero(_) | Expressao::Texto(_) | Expressao::Booleano(_) => 1,
            Expressao::OperacaoBinaria { esquerda, direita, .. } => {
                1 + self.visitar(esquerda) + self.visitar(direita)
            }
            Expressao::OperacaoUnaria { operando, .. } => 1 + self.visitar(operando),
            Expressao::ChamadaFuncao { argumentos, .. } => {
                1 + argumentos.iter().map(|a| self.visitar(a)).sum::<usize>()
            }
            Expressao::SeEntao { condicao, entao, senao } => {
                1 + self.visitar(condicao) + self.visitar(entao)
                    + senao.as_ref().map_or(0, |s| self.visitar(s))
            }
        }
    }
}

/// Função auxiliar para construir expressões
fn num(n: f64) -> Expressao {
    Expressao::Numero(n)
}

fn binop(esq: Expressao, op: Operador, dir: Expressao) -> Expressao {
    Expressao::OperacaoBinaria {
        esquerda: Box::new(esq),
        operador: op,
        direita: Box::new(dir),
    }
}

fn main() {
    // Construindo AST: max(3 + 4, 2 * 5)
    let ast = Expressao::ChamadaFuncao {
        nome: "max".to_string(),
        argumentos: vec![
            binop(num(3.0), Operador::Soma, num(4.0)),
            binop(num(2.0), Operador::Multiplicacao, num(5.0)),
        ],
    };

    let impressor = ImpressorFormatado::novo();
    let avaliador = Avaliador;
    let contador = ContadorNos;

    // Mesmo AST, três operações diferentes — sem modificar a AST!
    println!("Expressão: {}", impressor.visitar(&ast));
    println!("Resultado: {}", avaliador.visitar(&ast).unwrap());
    println!("Nós: {}", contador.visitar(&ast));

    // AST mais complexa: if (10 > 5) { 10 + 20 } else { 0 }
    let ast_condicional = Expressao::SeEntao {
        condicao: Box::new(binop(num(10.0), Operador::Maior, num(5.0))),
        entao: Box::new(binop(num(10.0), Operador::Soma, num(20.0))),
        senao: Some(Box::new(num(0.0))),
    };

    println!("\nExpressão: {}", impressor.visitar(&ast_condicional));
    println!("Resultado: {}", avaliador.visitar(&ast_condicional).unwrap());
    println!("Nós: {}", contador.visitar(&ast_condicional));
}

Diagrama

    Visitor Pattern — Duplo Despacho:

    ┌──────────────────┐     ┌───────────────────────┐
    │   Expressao      │     │ VisitanteExpressao     │
    │   (enum)         │     │ (trait)                │
    │──────────────────│     │───────────────────────│
    │ ◆ Numero         │     │ + visitar(&Expressao) │
    │ ◆ Texto          │     │   -> Resultado        │
    │ ◆ Booleano       │     └──────────┬────────────┘
    │ ◆ OperacaoBinaria│                │
    │ ◆ OperacaoUnaria │     ┌──────────┼──────────┐
    │ ◆ ChamadaFuncao  │     │          │          │
    │ ◆ SeEntao        │     ▼          ▼          ▼
    └──────────────────┘  ┌────────┐ ┌────────┐ ┌────────┐
                          │Impressor│ │Avaliador│ │Contador│
                          │Formatado│ │         │ │Nos     │
                          └────────┘ └────────┘ └────────┘

    O match no enum faz o "despacho" pelo tipo do nó:

    visitor.visitar(expr)
        │
        └──▶ match expr {
                Numero(_)          => ... lógica para número
                Texto(_)           => ... lógica para texto
                OperacaoBinaria {} => ... lógica para binop
                ...                => ... (exaustivo!)
             }

    Cada visitor implementa sua própria lógica para cada variante.
    Novos visitors podem ser adicionados sem mudar a AST.

Exemplo do Mundo Real

Um formatador de código simplificado que visita nós de uma AST e produz código formatado com indentação:

/// Formatador de código com configuração de estilo
struct FormatadorCodigo {
    largura_indentacao: usize,
    usar_espacos: bool,
    max_largura_linha: usize,
}

impl FormatadorCodigo {
    fn novo() -> Self {
        FormatadorCodigo {
            largura_indentacao: 4,
            usar_espacos: true,
            max_largura_linha: 80,
        }
    }

    fn indentar(&self, nivel: usize) -> String {
        let char_indent = if self.usar_espacos { ' ' } else { '\t' };
        let largura = if self.usar_espacos {
            self.largura_indentacao * nivel
        } else {
            nivel
        };
        std::iter::repeat(char_indent).take(largura).collect()
    }

    fn formatar_expressao(&self, expr: &Expressao, nivel: usize) -> String {
        let indent = self.indentar(nivel);
        match expr {
            Expressao::Numero(n) => format!("{}", n),
            Expressao::Texto(s) => format!("\"{}\"", s),
            Expressao::Booleano(b) => format!("{}", b),
            Expressao::OperacaoBinaria { esquerda, operador, direita } => {
                let esq = self.formatar_expressao(esquerda, nivel);
                let dir = self.formatar_expressao(direita, nivel);
                let op = match operador {
                    Operador::Soma => "+",
                    Operador::Subtracao => "-",
                    Operador::Multiplicacao => "*",
                    Operador::Divisao => "/",
                    Operador::Igual => "==",
                    Operador::Maior => ">",
                    Operador::Menor => "<",
                };

                let inline = format!("{} {} {}", esq, op, dir);
                if inline.len() + indent.len() <= self.max_largura_linha {
                    inline
                } else {
                    // Quebra em múltiplas linhas se exceder largura
                    format!(
                        "{}\n{}{} {}",
                        esq,
                        self.indentar(nivel + 1),
                        op,
                        dir,
                    )
                }
            }
            Expressao::ChamadaFuncao { nome, argumentos } => {
                let args: Vec<String> = argumentos.iter()
                    .map(|a| self.formatar_expressao(a, nivel + 1))
                    .collect();

                let inline = format!("{}({})", nome, args.join(", "));
                if inline.len() + indent.len() <= self.max_largura_linha {
                    inline
                } else {
                    let args_formatados = args.join(&format!(",\n{}", self.indentar(nivel + 1)));
                    format!(
                        "{}(\n{}{}\n{})",
                        nome,
                        self.indentar(nivel + 1),
                        args_formatados,
                        indent,
                    )
                }
            }
            Expressao::SeEntao { condicao, entao, senao } => {
                let mut resultado = format!(
                    "if {} {{\n{}{}\n{}}}",
                    self.formatar_expressao(condicao, nivel),
                    self.indentar(nivel + 1),
                    self.formatar_expressao(entao, nivel + 1),
                    indent,
                );
                if let Some(s) = senao {
                    resultado.push_str(&format!(
                        " else {{\n{}{}\n{}}}",
                        self.indentar(nivel + 1),
                        self.formatar_expressao(s, nivel + 1),
                        indent,
                    ));
                }
                resultado
            }
            _ => format!("{:?}", expr),
        }
    }
}

fn main() {
    let formatador = FormatadorCodigo::novo();

    let ast = Expressao::SeEntao {
        condicao: Box::new(binop(num(10.0), Operador::Maior, num(5.0))),
        entao: Box::new(Expressao::ChamadaFuncao {
            nome: "println".to_string(),
            argumentos: vec![Expressao::Texto("Maior!".to_string())],
        }),
        senao: Some(Box::new(Expressao::ChamadaFuncao {
            nome: "println".to_string(),
            argumentos: vec![Expressao::Texto("Menor ou igual".to_string())],
        })),
    };

    println!("Código formatado:");
    println!("{}", formatador.formatar_expressao(&ast, 0));
}

Quando Usar

  • Múltiplas operações sobre estrutura estável: A AST raramente muda, mas novas operações são frequentes
  • Separação de responsabilidades: Manter lógica de travessia separada da lógica de processamento
  • Análise e transformação de árvores: Compiladores, formatadores, linters, transpiladores
  • Relatórios variados: Gerar diferentes formatos (JSON, XML, texto) a partir da mesma estrutura

Quando NÃO Usar

  • Estrutura muda frequentemente: Se novos tipos de nó são adicionados constantemente, todos os visitors precisam ser atualizados
  • Única operação: Se há apenas uma operação, um método direto na estrutura é mais simples
  • Estruturas simples: Para estruturas planas (sem recursão), iteradores são suficientes
  • Performance crítica: A indireção de trait objects pode impactar performance em hot paths

Variações em Rust

Visitor mutável para transformações

/// Visitor que transforma a AST (otimizador)
trait TransformadorExpressao {
    fn transformar(&self, expr: Expressao) -> Expressao;
}

/// Otimizador: avalia expressões constantes em compilação
struct OtimizadorConstantes;

impl TransformadorExpressao for OtimizadorConstantes {
    fn transformar(&self, expr: Expressao) -> Expressao {
        match expr {
            Expressao::OperacaoBinaria { esquerda, operador, direita } => {
                let esq = self.transformar(*esquerda);
                let dir = self.transformar(*direita);

                // Se ambos são números, calcula em compilação
                if let (Expressao::Numero(a), Expressao::Numero(b)) = (&esq, &dir) {
                    match operador {
                        Operador::Soma => return Expressao::Numero(a + b),
                        Operador::Multiplicacao => return Expressao::Numero(a * b),
                        _ => {}
                    }
                }

                Expressao::OperacaoBinaria {
                    esquerda: Box::new(esq),
                    operador,
                    direita: Box::new(dir),
                }
            }
            outro => outro,
        }
    }
}

Padrões Relacionados

  • Iterator: Iteradores percorrem sequências linearmente; Visitors percorrem árvores
  • Composite: Visitor é frequentemente usado com estruturas Composite (árvores)
  • Strategy: Cada visitor é essencialmente uma strategy aplicada à estrutura
  • Command: Visitors podem ser tratados como comandos aplicados a estruturas

Conclusão

O Visitor Pattern em Rust se beneficia enormemente de enums e pattern matching. A abordagem com enum é a mais idiomática: cada visitor implementa um match exaustivo sobre as variantes da AST, e o compilador garante que nenhum caso seja esquecido. Quando um novo tipo de nó é adicionado ao enum, todos os visitors quebram em compilação, forçando a atualização. Esta é uma vantagem significativa sobre linguagens com herança, onde um novo tipo pode silenciosamente usar uma implementação padrão incorreta. Para ASTs e estruturas similares, o Visitor com enum é a escolha clara em Rust.