web-dev-qa-db-ja.com

AST訪問者パターンの処理と有用性

ビジターパターンは通常、異種オブジェクトの階層をトラバースし(同じ抽象オブジェクトを継承)、これらのオブジェクトの処理をオブジェクト内のデータから分離するために使用されます。ビジターパターン(よく引用される)の典型的な使用法は、コンパイラーでの抽象構文ツリーの処理です。実際、ツリーの構造は実行時にのみ(プログラムが解析された後)知られており、ビジターとして実装されたセマンティックパスに従ってノードを変更してツリーをトラバースしたいと考えています。しかし、ASTNodeタイプとVisitorタイプの関数として二重(動的)ディスパッチを許可するか、AST。これをC++でコーディングする必要がある場合(コンパイルせずにその場で行う)、私は次のようにします:

struct AstNode
{
    //virtual void accept() = 0; //this won't work since template does not override virtual pure method
    virtual ~AstNode();
};

struct SpecificNode : public AstNode
{
    template <class Visitor>
    void accept(Visitor v)
    {
        v.visit(*this);
    }
};

//other AST nodes

class MyVisitor
{
    void visit(SpecificNode n) {/*...*/}

    //other visit methods for other AST nodes
};

//other visitors

ここでは二重派遣はありません。それは真の訪問者ではありません。しかし、なぜこの二重派遣が必要になるのかは理解できます。

ノート :

このウィキペディアのページには、ダブルディスパッチの他の用途がリストされていますが、AST: http://en.wikipedia.org/wiki/Double_dispatch#Examples は含まれていません。

5
matovitch

訪問者パターンがしばしば語られないことの1つは、式の問題のどちら側に取り組むかを選択できるようにすることです。

では、表現の問題とは何でしょうか。これは、拡張性の基本的な問題を指します。私たちのプログラムは、操作を使用してデータ型を操作します。プログラムが進化するにつれて、新しいデータ型と新しい操作でプログラムを拡張する必要があります。特に、既存のデータ型で機能する新しい操作を追加できるようにしたいし、既存の操作で機能する新しいデータ型を追加したい。 Andこれを真にしたいextension、つまりexistingプログラムを変更したくない、既存の抽象化を尊重したい、エクステンションを個別のモジュール、個別の名前空間、個別にコンパイル、個別にデプロイ、個別にタイプチェックする必要があります。私たちはそれらをタイプセーフにしたいと考えています。

式の問題は、実際に言語でそのような拡張性をどのように提供するのですか?

手続き型プログラミングや関数型プログラミングの典型的な素朴な実装の場合、新しい操作(プロシージャ、関数)を追加するのは非常に簡単ですが、新しいデータ型を追加するのは非常に困難です。ケースの区別(switchcase、パターンマッチング)の一種であり、新しいケースを追加する必要があります。つまり、既存のコードを変更します。

func print(node):
  case node of:
    AddOperator => print(node.left) + '+' + print(node.right)
    NotOperator => '!' + print(node)

func eval(node):
  case node of:
    AddOperator => eval(node.left) + eval(node.right)
    NotOperator => !eval(node)

これで、新しい操作、たとえば型チェックを追加する場合は簡単ですが、新しいノードタイプを追加する場合は、すべての操作で既存のすべてのパターンマッチング式を変更する必要があります。

そして、典型的なナイーブOOの場合、正反対の問題があります。既存の操作で機能する新しいデータ型を簡単に追加できます(継承または上書きすることにより)が、基本的に変更を意味するため、新しい操作を追加することは困難です。既存のクラス/オブジェクト。

class AddOperator(left: Node, right: Node) < Node:
  meth print:
    left.print + '+' + right.print

  meth eval
    left.eval + right.eval

class NotOperator(expr: Node) < Node:
  meth print:
    '!' + expr.print

  meth eval
    !expr.eval

ここでは、必要なすべての操作を継承、オーバーライド、または実装するため、新しいノードタイプの追加は簡単ですが、すべてのリーフクラスまたは基本クラスのいずれかに追加する必要があるため、新しい操作の追加は難しく、既存の操作を変更します。コード。

いくつかの言語には、式の問題を解決するためのいくつかの構成要素があります。Haskellには型クラスがあり、Scalaには暗黙の引数があり、ラケットにはユニットがあり、Goにはインターフェースがあり、CLOSとClojureにはマルチメソッドがあります。

ただし、OO しないという言語では、式の問題を解決する方法があります(JavaまたはC#など) 、ビジターパターンでは、少なくとも「毒を拾う」ことができます。パターンが行うことは、デザインを90度横に回転させることです。operationsになるclassesPrintVisitorEvalVisitor)そして逆にtypesmethodsvisitAddOperatorvisitNotOperator(またはvisit(言語が引数ベースのオーバーロードをサポートしている場合))これはnot式の問題を解決します(つまり、追加を簡単にする方法bothタイプと操作)、ただし行うどちらかを選択して簡単にすることができます。

したがって、言語doesが式の問題を解決する方法をサポートしている場合、この回避策は必要ありません。

ただし、これはビジターパターンが行う唯一のことではありません。


注:C++についての言及がまったくないことは明らかです。残念ながら、私はそれについて十分に知りません。オーバーロードと引数ベースのディスパッチ、仮想継承、フリー関数、マクロ、そして最も重要なコンパイル時のテンプレートメタプログラミングの間で、式の問題isがC++で解決されたと思いますが、わかりません確かに。

問題は、誰かが式の問題の解決策を見つけると、それを再定義してそれをさらに困難にして解決し、新しい解決策がさらに強力で表現力豊かになることです。たとえば、Haskellコミュニティによる元の定式化はモジュール型チェックを必要としませんでしたが、Scalaコミュニティは、式の問題に型と演算のモジュール拡張(個別のコンパイルなど)だけを含めるべきではないことを提案しましただけでなく、モジュラー型チェックとそれらの拡張機能の型推論もあり、現時点ではScalaの暗黙的クラスだけが実行でき、Haskellの型クラスとMLのファンクターは実行できません。

14
Jörg W Mittag