추상 구문 트리에 대한 방문자 패턴 구현


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. : 조금 정교하게 말씀해 주시겠습니까?
marco-fiset

답변:


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방법 매개 변수는 위의 예에서 사용 된, 그러나 다만 저를 믿지 : 하나를 가지고하는 것은 매우 유용하다. 예를 들어, 오류를보고하는 로거 인스턴스 일 수 있습니다.


나는 비슷한 것을 구현하고 지금까지 결과에 매우 만족합니다. 감사!
marco-fiset

6

이전에는 재귀 트리에서 방문자 패턴을 구현했습니다.

내 특정 재귀 데이터 구조는 매우 간단했습니다. 일반 노드, 자식이있는 내부 노드 및 데이터가있는 리프 노드의 세 가지 노드 유형입니다. 이것은 AST가 예상하는 것보다 훨씬 간단하지만 아이디어가 확장 될 수 있습니다.

내 경우에는 의도적으로 자녀가있는 노드의 수락이 자녀에게 수락을 호출하거나 수락 내에서 방문자에게 전화를 걸지 못하게했습니다. 방문중인 노드의 하위에 승인을 위임하는 것은 방문자의 올바른 "방문"멤버 구현의 책임입니다. 다른 방문자 구현이 트리 표현과 독립적으로 방문 순서를 결정할 수 있도록하기 위해이 방법을 선택했습니다.

두 번째 이점은 내 트리 노드 내에 방문자 패턴의 아티팩트가 거의 없다는 것입니다. 각 "수락"은 정확한 콘크리트 유형으로 방문자의 "방문"을 호출합니다. 이를 통해 방문 논리를보다 쉽게 ​​찾고 이해할 수 있으며 방문자 구현 내부에 있습니다.

명확성을 위해 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
일에 대한 allow different Visitor implementations to be able to decide the order of visitation. 아주 좋은 생각입니다.
marco-fiset

@ marco-fiset 그러면 알고리즘 (방문자)은 데이터 (노드)가 어떻게 구성되어 있는지 알아야합니다. 이것은 방문자 패턴이 제공하는 알고리즘 데이터 분리를 분석합니다.
B Visschers

2
@BVisschers 방문자는 각 노드 유형에 대한 기능을 구현하므로 주어진 시간에 어떤 노드에서 작동하는지 알고 있습니다. 아무것도 깨지 않습니다.
marco-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);
}

언어에 깊이 중첩 된 구문이있는 경우 파서에서 실패 할 수 있습니다. 언어의 호출 스택과 독립적으로 스택을 유지 관리해야 할 수 있습니다.
피트 Kirkham

1
@PeteKirkham : 꽤 깊은 나무 여야합니다.
Robert Harvey

@PeteKirkham 실패 할 수 있다는 것은 무엇을 의미합니까? 어떤 종류의 StackOverflowException 또는 개념이 제대로 확장되지 않는다는 것을 의미합니까? 나는 성능에 신경 쓰지 않는 순간, 재미와 학습을 위해서만 이것을합니다.
marco-fiset

@ marco-fiset 네, 말할 경우 스택 오버플로 예외가 발생합니다. 방문자와 함께 크고 깊은 XML 파일을 구문 분석하십시오. 대부분의 프로그래밍 언어에는 적용되지 않습니다.
피트 Kirkham
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.