web-dev-qa-db-fra.com

Implémentation du modèle de visiteur pour une arborescence de syntaxe abstraite

Je suis en train de créer mon propre langage de programmation, ce que je fais à des fins d'apprentissage. J'ai déjà écrit le lexer et un analyseur de descente récursif pour un sous-ensemble de ma langue (je supporte actuellement des expressions mathématiques, telles que + - * / et parenthèses). L'analyseur me rend un arbre de syntaxe abstraite, sur lequel j'appelle la méthode Evaluate pour obtenir le résultat de l'expression. Tout fonctionne bien. Voici approximativement ma situation actuelle (exemples de code en C #, bien que cela soit à peu près indépendant du langage):

public abstract class Node
{
    public abstract Double Evaluate();
}

public class OperationNode : Node
{
    public Node Left { get; set; }
    private String Operator { get; set; }
    private Node Right { get; set; }

    public Double Evaluate()
    {
        if (Operator == "+")
            return Left.Evaluate() + Right.Evaluate();

        //Same logic for the other operators
    }
}

public class NumberNode : Node
{
    public Double Value { get; set; }

    public Double Evaluate()
    {
        return Value;
    }
}

Cependant, je voudrais découpler l'algorithme des nœuds d'arbre parce que je veux appliquer le principe ouvert/fermé, donc je n'ai pas à rouvrir chaque classe de nœuds lorsque je veux implémenter la génération de code par exemple. J'ai lu que le modèle de visiteur est bon pour cela. J'ai une bonne compréhension du fonctionnement du modèle et de l'utilisation de la double répartition. Mais en raison de la nature récursive de l'arbre, je ne sais pas comment je dois l'approcher. Voici à quoi ressemblerait mon visiteur:

public class AstEvaluationVisitor
{
    public void VisitOperation(OperationNode node)
    {
        // Here is where I operate on the operation node.
        // How do I implement this method?
        // OperationNode has two child nodes, which may have other children
        // How do I work the Visitor Pattern around a recursive structure?

        // Should I access children nodes here and call their Accept method so they get visited? 
        // Or should their Accept method be called from their parent's Accept?
    }

    // Other Visit implementation by Node type
}

C'est donc mon problème. Je veux y faire face immédiatement alors que ma langue ne supporte pas beaucoup de fonctionnalités pour éviter d'avoir un problème plus important plus tard.

Je n'ai pas posté ceci sur StackOverflow car je ne veux pas que vous fournissiez une implémentation. Je veux seulement que vous partagiez des idées et des concepts que j'aurais pu manquer et comment je devrais aborder cela.

25
marco-fiset

C'est à l'implémentation du visiteur de décider s'il faut visiter les nœuds enfants et dans quel ordre. C'est tout l'intérêt du modèle de visiteur.

Afin d'adapter le visiteur à plus de situations, il est utile (et assez courant) d'utiliser des génériques comme celui-ci (c'est Java):

public interface ExpressionNodeVisitor<R, P> {
    R visitNumber(NumberNode number, P p);
    R visitBinary(BinaryNode expression, P p);
    // ...
}

Et une méthode accept ressemblerait à ceci:

public interface ExpressionNode extends Node {
    <R, P> R accept(ExpressionNodeVisitor<R, P> visitor, P p);
    // ...
}

Cela permet de passer des paramètres supplémentaires au visiteur et d'en récupérer un résultat. Ainsi, l'évaluation de l'expression peut être implémentée comme ceci:

public class EvaluatingVisitor
    implements ExpressionNodeVisitor<Double, Void> {
    public Double visitNumber(NumberNode number, Void p) {
        // Parse the number and return it.
        return Double.valueOf(number.getText());
    }
    public Double visitBinary(BinaryNode binary, Void p) {
        switch (binary.getOperator()) {
        case '+':
            return binary.getLeftOperand().accept(this, p)
                + binary.getRightOperand().accept(this, p);
        // More cases for other operators here.
        }
    }
}

Le paramètre de méthode accept n'est pas utilisé dans l'exemple ci-dessus, mais croyez-moi: il est très utile d'en avoir un. Par exemple, il peut s'agir d'une instance de Logger à laquelle signaler des erreurs.

11
lorus

J'ai déjà implémenté le modèle de visiteur sur un arbre récursif.

Ma structure de données récursive particulière était extrêmement simple - seulement trois types de nœuds: le nœud générique, un nœud interne qui a des enfants et un nœud feuille qui a des données. C'est beaucoup plus simple que ce que j'attends de votre AST être, mais peut-être que les idées peuvent évoluer.

Dans mon cas, je n'ai délibérément pas laissé l'accepter d'un nœud avec des enfants appeler Accept sur ses enfants, ou appeler visiteur.Visit (enfant) depuis l'intérieur de Accept. Il est de la responsabilité de l'implémentation correcte du membre "Visit" du visiteur de déléguer les Accepte aux enfants du nœud visité. J'ai choisi cette voie parce que je voulais permettre à différentes implémentations de Visiteur de pouvoir décider de l'ordre de visite indépendamment de l'arborescence.

Un avantage secondaire est qu'il n'y a presque aucun artefact du modèle Visitor à l'intérieur de mes nœuds d'arbre - chaque "Accept" appelle simplement "Visit" sur le visiteur avec le type de béton correct. Cela facilite la localisation et la compréhension de la logique de visite, tout est à l'intérieur de l'implémentation du visiteur.

Pour plus de clarté, j'ai ajouté un pseudocode C++ - ish. D'abord les nœuds:

class INode {
  public:
    virtual void Accept(IVisitor& i_visitor) = 0;
};

class NodeWithChildren : public INode {
  public:
     virtual void Accept(IVisitor& i_visitor) override {
        i_visitor.Visit(*this);
     }
     // Plus interface for getting the children, exercise for the reader ;-)
 };

 class LeafNode : public INode {
   public:
     virtual void Accept(IVisitor& i_visitor) override {
       i_visitor.Visit(*this);
     }
 };

Et le visiteur:

class IVisitor {
  public:
     virtual void Visit(NodeWithChildren& i_node) = 0;
     virtual void Visit(LeafNode& i_node) = 0;
};

class ConcreteVisitor : public IVisitor
  public:
     virtual void Visit(NodeWithChildren& i_node) override {
       // Do something useful, then...
       for(Node * p_child : i_node) {
         child->Accept(*this);
       }
     }

     virtual void Visit(LeafNode& i_node) override {
        // Just do something useful, there are no children.
     }

};
6
Joris Timmermans

Vous travaillez le modèle visiteur autour d'une structure récursive de la même manière que vous feriez n'importe quoi d'autre avec votre structure récursive: en visitant les nœuds de votre structure récursivement.

public class OperationNode
{
    public int SomeProperty { get; set; }
    public List<OperationNode> Children { get; set; }
}

public static void VisitNode(OperationNode node)
{
    ... Visit this node

    foreach(var node in Children)
    {
         VisitNode(node);
    }
}

public static void VisitAllNodes()
{
    VisitNode(rootNode);
}
3
Robert Harvey