web-dev-qa-db-ja.com

抽象構文ツリーのビジターパターンの実装

私は自分のプログラミング言語を作成している最中です。これは、学習目的で行います。言語のサブセット用のレクサーと再帰降下パーサーをすでに書いています(現在、+ - * /や括弧などの数式をサポートしています)。パーサーから抽象構文ツリーが返され、そこでEvaluateメソッドを呼び出して式の結果を取得します。すべてが正常に動作します。おおよその現在の状況は次のとおりです(C#のコード例ですが、これはほとんど言語に依存しません)。

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

ただし、たとえば、コード生成を実装するときにすべてのノードクラスを再度開く必要がないように、Open/Closed Principleを適用したいので、アルゴリズムをツリーノードから切り離したいと思います。私はビジターパターンがそのために良いと読んだ。私はパターンがどのように機能するかをよく理解しており、ダブルディスパッチを使用するのがよい方法であることを理解しています。しかし、ツリーは再帰的な性質を持っているため、どのように対処すればよいかわかりません。私の訪問者は次のようになります。

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
}

これが私の問題です。私の言語は多くの機能をサポートしていませんが、後で大きな問題が発生するのを避けるため、すぐに取り組みたいと思います。

実装を提供してほしくないので、StackOverflowに投稿しませんでした。私が見逃したかもしれないアイデアやコンセプト、そして私がこれにどのように取り組むべきかを共有してほしい。

25
marco-fiset

子ノードを訪問するかどうか、およびその順序を決定するのは訪問者の実装です。これがビジターパターンの要点です。

ビジターをより多くの状況に適応させるために、次のようなジェネリック(Javaです)を使用すると便利です(かなり一般的です)。

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

また、acceptメソッドは次のようになります。

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

これにより、追加のパラメーターをビジターに渡し、そこから結果を取得できます。したがって、式の評価は次のように実装できます。

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

acceptメソッドパラメータは上記の例では使用されていませんが、私を信じてください:持っていると非常に便利です。たとえば、エラーを報告するLoggerインスタンスにすることができます。

11
lorus

以前に再帰ツリーにビジターパターンを実装しました。

私の特定の再帰的データ構造は非常にシンプルでした。3つのノードタイプ(ジェネリックノード、子を持つ内部ノード、データを持つリーフノード)だけです。これは私があなたのASTであると予想するよりもはるかに単純ですが、おそらくアイデアはスケーリングできます。

私の場合、子を持つノードのAcceptがその子に対してAcceptを呼び出すことや、Accept内からvisitor.Visit(child)を呼び出すことを意図的に許可しませんでした。訪問されているノードの子にAcceptsを委任するのは、訪問者の正しい「Visit」メンバー実装の責任です。私がこの方法を選択したのは、ツリーの表示とは関係なく、さまざまなVisitor実装が訪問の順序を決定できるようにしたかったからです。

2番目の利点は、ツリーノード内にビジターパターンのアーティファクトがほとんどないことです。それぞれの「Accept」は、正しい具象タイプでビジターの「Visit」を呼び出すだけです。これにより、訪問ロジックを簡単に見つけて理解することができます。すべて訪問者実装の内部にあります。

わかりやすくするために、C++風の疑似コードをいくつか追加しました。まずノード:

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

そして訪問者:

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

再帰構造で他のことを行うのと同じ方法で、再帰構造の周りのビジターパターンを操作します。つまり、構造内のノードに再帰的にアクセスします。

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