抽象構文ツリーの訪問者パターンの実装


23

私は自分のプログラミング言語を作成している最中で、それは学習目的のためにしています。私はすでに自分の言語のサブセット用にレクサーと再帰降下パーサーを作成しました(現在、などの数式をサポートし+ - * /ています)。パーサーは、抽象構文ツリーを返します。抽象ツリーでは、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に投稿しませんでした。私が見落としていたかもしれないアイデアや概念、そしてこれにどうアプローチするかだけを共有してほしい。


1
私はおそらく代わりにツリーの折り畳みを実装します
jk

@jk .:少し詳しく説明していただけますか?
マルコフィセット

回答:


10

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

訪問者をより多くの状況に適応させるには、次のようなジェネリック(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メソッドのパラメータは、上記の例で使用されるが、ちょうど私を信じていない:1を持っていることは非常に便利です。たとえば、エラーを報告するLoggerインスタンスにすることができます。


似たようなものを実装することになり、これまでの結果に非常に満足しています。ありがとう!
マルコフィセット

6

以前に再帰ツリーに訪問者パターンを実装しました。

私の特定の再帰的データ構造は非常に単純でした。汎用ノード、子を持つ内部ノード、データを持つリーフノードの3つのノードタイプのみです。これは私があなたのASTが期待するよりもはるかに簡単ですが、おそらくアイデアは拡大縮小できます。

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

副次的な利点は、ツリーノード内に訪問者パターンのアーティファクトがほとんどないことです。各「承認」は、正しい具体的なタイプで訪問者の「訪問」を呼び出すだけです。これにより、訪問ロジックを簡単に見つけて理解できるようになり、すべて訪問者の実装内にあります。

明確にするために、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.
     }

};

1
+1 allow different Visitor implementations to be able to decide the order of visitation。非常に良いアイデア。
マルコフィセット

@ marco-fisetアルゴリズム(訪問者)は、データ(ノード)がどのように構成されているかを知る必要があります。これにより、訪問者パターンが与えるアルゴリズムとデータの分離が破壊されます。
B Visschers 14

2
@BVisschers訪問者は各ノードタイプに関数を実装するため、どのノードで操作されているかを常に把握しています。何も壊しません。
マルコ・fiset

3

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

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

言語に深くネストされた構造がある場合、これはパーサーで失敗する可能性があります-言語の呼び出しスタックとは無関係にスタックを維持する必要がある場合があります。
ピートカーカム

1
@PeteKirkham:それはかなり深いツリーでなければなりません。
ロバートハーベイ

@PeteKirkham失敗するとはどういうことですか?何らかの種類のStackOverflowExceptionを意味しますか、それともコンセプトがうまくスケールしないということですか?現時点では、パフォーマンスを気にせず、楽しみと学習のためだけにこれを行います。
マルコフィセット

@ marco-fisetはい、訪問者で大規模で深いXMLファイルを解析しようとすると、スタックオーバーフロー例外が発生します。ほとんどのプログラミング言語で使用できます。
ピートカーカム
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.