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


23

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 les expressions mathématiques, telles que + - * /et les parenthèses). L'analyseur me rend un arbre de syntaxe abstraite, sur lequel j'appelle la Evaluateméthode 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.


1
J'implémenterais probablement un repli d'arbre à la place
jk.

@jk .: Pourriez-vous élaborer un peu?
marco-fiset

Réponses:


10

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 acceptméthode 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 acceptparamètre de méthode 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.


J'ai fini par mettre en œuvre quelque chose de similaire et je suis très satisfait du résultat jusqu'à présent. Merci!
marco-fiset

6

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, 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.Visiter (enfant) à partir 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 visiteurs de pouvoir décider de l'ordre des visites indépendamment de la représentation arborescente.

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.
     }

};

1
+1 pour allow different Visitor implementations to be able to decide the order of visitation. Très bonne idée.
marco-fiset

@ marco-fiset L'algorithme (visiteur) devra alors savoir comment les données (nœuds) sont structurées. Cela détruira la séparation algorithme-données que le modèle de visiteur donne.
B Visschers

2
@BVisschers Les visiteurs implémentent une fonction pour chaque type de nœud, afin qu'il sache sur quel nœud il opère à un moment donné. Ça ne casse rien.
marco-fiset

3

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);
}

Cela peut échouer pour les analyseurs si le langage a des constructions profondément imbriquées - il peut être nécessaire de maintenir une pile indépendamment de la pile d'appels du langage.
Pete Kirkham

1
@PeteKirkham: Cela devrait être un arbre assez profond.
Robert Harvey

@PeteKirkham Que voulez-vous dire que cela peut échouer? Voulez-vous dire une sorte d'exception StackOverflowException ou que le concept ne s'adaptera pas bien? Pour le moment je ne me soucie pas de la performance, je ne fais cela que pour le plaisir et l'apprentissage.
marco-fiset

@ marco-fiset Oui, vous obtenez une exception de dépassement de pile si vous dites, essayez d'analyser un gros fichier XML profond avec un visiteur. Vous vous en sortirez pour la plupart des langages de programmation.
Pete Kirkham
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.