依存データ構造を最新に保つにはどうすればよいですか?


8

構文解析ツリー、抽象構文ツリー、および制御フローグラフがあり、それぞれが前のものから論理的に派生しているとします。原則として、解析ツリーがあれば各グラフを作成するのは簡単ですが、解析ツリーが変更されたときにグラフを更新する複雑さをどのように管理できますか?私たちはツリーがどのように変更されたかを正確に知っていますが、管理が難しくならない方法で変更を他のツリーにどのように伝播できますか?

当然ながら、依存グラフは最初のグラフが変更されるたびに最初から再構築するだけで更新できますが、依存グラフの変更の詳細を知る方法はありません。

現在、この問題を解決する方法は4つありますが、それぞれに問題があります。

  1. 従属ツリーのノードはそれぞれ、元のツリーの関連ノードを監視し、必要に応じて自身と元のツリーノードのオブザーバーリストを更新します。これの概念的な複雑さは困難になる可能性があります。
  2. 元のツリーの各ノードには、それに依存する従属ツリーノードのリストがあり、ノードが変更されると、従属ノードにフラグを設定して、従属ノードの親を含め、ダーティとしてマークします。ルートに。変更のたびに、依存グラフを最初から作成するアルゴリズムとよく似たアルゴリズムを実行しますが、クリーンノードをスキップして各ダーティノードを再構築し、再構築されたノードが実際にダーティノードと異なるかどうかを追跡します。これも注意が必要です。
  3. 元のグラフと従属グラフの間の論理的な接続を、おそらく宣言型言語を使用して設計された制約のリストのようなデータ構造として表すことができます。元のグラフが変更された場合、違反している制約と違反を修正するために依存ツリーをどのように変更する必要があるかを見つけるためにリストをスキャンするだけで、すべてデータとしてエンコードされます。
  4. 既存の依存グラフがないかのように、依存グラフを最初から再構築し、既存のグラフと新しいグラフを比較して、どのように変化したかを確認できます。違いを検出するために利用できるアルゴリズムがあることを知っているので、これが最も簡単な方法であると確信していますが、それらはすべて非常に計算コストが高く、原則として不要と思われるため、このオプションは意図的に避けています。

この種の問題に対処する正しい方法は何ですか?確かに、このすべてをほぼ簡単にするデザインパターンがなければなりません。この一般的な説明のすべての問題に対して適切な解決策があると便利です。このクラスの問題には名前がありますか?


この問題が引き起こすトラブルについて詳しく説明しましょう。この問題は、プロジェクトの2つの部分がグラフを操作するたびにさまざまな場所で発生します。各グラフは、ソフトウェアの実行中に変化する同じものの異なる表現です。これはインターフェースのアダプターを作成するようなものですが、単一のオブジェクトまたは固定数のオブジェクトをラップする代わりに、任意のサイズのグラフ全体をラップする必要があります。

私がこれを試す度に、私は混乱して維持不可能な混乱に終わります。オブザーバーの制御フローは、複雑になると追跡が困難になる可能性があります。あるグラフを別のグラフに変換するアルゴリズムは、通常、レイアウトが明確で複数のクラスにまたがっていない場合に追跡するには十分な注意が必要です。問題は、元のグラフが変更されているときに、単純で単純なグラフ変換アルゴリズムだけを使用する方法がないように見えることです。

当然のことながら、通常のグラフ変換アルゴリズムを直接使用することはできません。ゼロから開始する以外の方法で変更に対応できないためです。代わりの方法は何ですか?おそらく、アルゴリズムは継続渡しスタイルで記述できます。この場合、アルゴリズムの各ステップは、ビジターのように、元のグラフのノードのタイプごとにメソッドを持つオブジェクトとして表されます。次に、さまざまな単純なビジターを組み合わせてアルゴリズムを組み立てることができます。


別の例:JPanelsとレイアウトマネージャーを使用して、Java Swingの場合と同じようにレイアウトされたGUIがあるとします。複雑なレイアウトマネージャーの代わりにネストされたJPanelsを使用することでそのプロセスを簡略化できるため、レイアウト目的でのみ存在し、それ以外の場合は無意味なノードを含むさまざまなコンテナーのツリーになります。ここで、GUIの生成に使用されたものと同じツリーがアプリケーションの別の部分でも使用されていると想定しますが、ツリーをグラフィカルにレイアウトする代わりに、抽象表現ツリーをフォルダーのシステムとして生成するライブラリーを操作します。このライブラリを使用するには、レイアウトノードを持たないバージョンのツリーが必要です。レイアウトノードを親ノードにフラット化する必要があります。


もう1つの見方:可変ツリーを操作するというまさにその概念は、デメテル法則に違反しています。構文解析ツリーや構文ツリーが通常のように値である場合は、実際には法律違反にはなりませんが、その場合は何も最新の状態に保つ必要がないため問題はありません。それで、この問題はデメテルの法則に違反した直接の結果として存在しますが、ドメインがツリーまたはグラフの操作に関するものであるように思われる場合、一般的にどのようにそれを回避しますか?

複合パターンは、 1つのオブジェクトにグラフを回すとデメテルの法則に従うための素晴らしいツールです。ある種類のツリーを別の種類のツリーに効果的に変換するために複合パターンを使用することは可能ですか?抽象構文木や制御フローグラフのように機能するように、複合解析ツリーを作成できますか?単一責任の原則に違反せずにそれを行う方法はありますか?複合パターンは、クラスが彼らが触れるすべての責任を吸収する傾向がありますが、おそらくそれは戦略パターンと何らかの形で組み合わせることができます。


1
おそらく、例のために、増分解析アルゴリズムを見てcstheory.stackexchange.com/questions/6852/...
PSR

回答:


5

あなたのシナリオでは、オブザーバーパターンのバリエーションについて議論していると思います。元の各ノード(「サブジェクト」)には、(少なくとも)次の2つのメソッドがあります。

  • registerObserver(observer) –オブザーバーのリストに依存ノードを追加します。
  • notifyObservers()x.notify(this)各オブザーバーの呼び出し

そして、各従属ノード(「オブザーバー」)にはnotify(original)メソッドがあります。シナリオの比較:

  1. このnotifyメソッドは、依存するサブツリーをすぐに再構築します。
  2. このnotifyメソッドはフラグを設定します。再計算のたびに再計算が行われます。
  3. このnotifyObservers方法はスマートで、制約が無効化されているオブザーバーにのみ通知します。これはおそらく訪問者パターンを使用するため、従属ノードはこれを決定するメソッドを提供できます。
  4. (このパターンはブルートフォースの再構築とは関係ありません)

最初の3つのアイデアはオブザーバーパターンのバリエーションにすぎないので、それらのデザインは同様の複雑さを持ちます(実際、それらは実際には複雑さを増すように順序付けられています。№1が実装が最も簡単だと思います)。

私は1つの拡張機能を考えることができます。依存ツリーを遅延して構築することです。その場合、各依存ノードには、validまたはに設定されたブールフラグがありますinvalid。各アクセサメソッドはこのフラグをチェックし、必要に応じてサブツリーを再計算します。№2との違いは、再計算は変更ではなくアクセス時に行われることです。これにより、計算が最も少なくなる可能性がありますが、ノードのタイプをアクセス時に変更する必要がある場合、重大な問題が発生する可能性があります。


複数の依存ツリーの必要性にも挑戦したいと思います。たとえば、パーサーは常にASTをすぐに出力するように構成します。このツリーの構築中にのみ関連する情報は、永続的なデータ構造に格納する必要はありません。同様に、ASTが制御フローグラフとして解釈されるようにオブジェクトを選択することもできます。

実際の例では、perlインタプリタ内のコンパイラ部分がこれを実行します。ASTはボトムアップで構築され、その間、一部のノードは定数で折りたたまれます。2回目の実行では、ノードは実行順に接続され、その間、一部のノードは最適化によってスキップされます。その結果、非常に高速な解析(およびいくつかの割り当て)が行われますが、最適化は非常に制限されます。そのような設計は可能です、おそらくあなたが努力すべきものではないことに注意してください:それは計算されたトレードオフ 完全な違反シングル責任原則

実際に複数のツリーが必要な場合は、それらを本当に同時に構築する必要があるかどうかも検討する必要があります。ほとんどの場合、解析ツリーは解析後も一定です。同様に、ASTはおそらく、マクロが解決され、ASTレベルの最適化が実行された後も一定のままです。


同じように、Functional Reactive Programmingを試すこともできます。それは、より柔軟であるかもしれない:lampwww.epfl.ch/~imaier/pub/DeprecatingObserversTR2010.pdf
ジム・バロウズ

2

2番目のグラフを最初のグラフから完全に導出できる2つのグラフの一般的なケースを考えているようで、最初の部分が変更されたときに2番目のグラフを効率的に再計算したいとします。

これは、最初のグラフだけで再計算を最小化する問題と概念的には違いはないように見えますが、特定のシステムに実装された場合、各グラフでタイプが異なると考えられます。

グラフ内およびグラフ間の両方で依存関係を追跡することがほとんどすべてです。変更されたノードごとに、すべての依存関係を再帰的に更新します。

もちろん、更新を行う前に、依存関係グラフをトポロジ的にソートする必要があります。これにより、循環依存関係があり、更新の無限の波が発生する可能性があるかどうかがわかります。また、どのノードでも、そのノードを更新する前にすべての依存関係を更新できるため、後でやり直す必要がある無意味な計算を回避できます。

特に依存関係を宣言型言語で表現する必要はありませんが、それは完全に独立した問題です。

これは一般的なアルゴリズムであり、特定のケースでは、スピードを上げるためにできることがもっとあるかもしれません。1つの依存関係を更新するために行っている作業の一部は、他の依存関係の更新にも役立つ可能性があり、優れたアルゴリズムはそれを利用します。

グラフ変換アルゴリズムが維持不可能な混乱である限り、解決策は言語固有ですが、オブジェクト指向のアプローチは、依存関係の更新、つまり依存関係の表現、トポロジカルソートの実行、計算のトリガーを純粋に処理するクラスをいくつか持つことです。 。計算を行うには、作成時に渡されたファーストクラス関数を使用して、実際のクラスにデリゲートします。おそらく、渡されたクラスがインターフェイスを実装する必要があるためです(できない場合は通常、たとえば、作成しなかった場合は、アダプターを使用できます)。場合によっては、リフレクションを使用してオブジェクト関係のグラフからグラフ情報を収集し、その方法でメソッドを呼び出すことができると思います。


1

ツリーがどのように変更されたかを正確に知っていると述べましたが、いつ知っていますか?

HashTreesやHash chain(Merkle Tree)や、一般的にはエラー検出の概念を試してみてはいかがでしょうか。木が大きい場合は、最初のグラフを言うN / 2ゾーンまたはルートNゾーンに分割し、それらのゾーンにハッシュ/チェックサムを割り当てます。依存ツリーは、最初のツリーのゾーンに依存する独自のN / 2またはルートNゾーンのセットを維持します。最初のツリーで変更が検出されたら、単純なルックアップを使用して依存ツリーの対応するノードを更新します(変更されたものと、そのゾーンのハッシュ/チェックサムがわかっているため)。


3
これがどのように機能するはずなのか、私にはよくわかりません。元のツリーと変更されたツリーの両方を使用して直接比較しているので、ハッシュの計算がどのように役立つかわかりません。
Geo

エラー検出の考え方は、何が変更されたかを検出することであり、したがって、あなたの目的のために、どこを変更すればよいかを把握し、その変更を管理します(これはあなたの質問でした)。上記の提案は思考実験です。もしツリーが十分に単純で、「変更されたもの」を公開できる自明なプロパティがある場合は、おそらくハッシュを計算する必要はありません。「エラー検出」または「変更検出」メカニズム/アルゴは、伝播の管理に役立ちます。
晴れ

1

問題の別の表現-いくつかのデータ(グラフ)とさまざまな表現(たとえば、レイアウトパネル/ツリービュー)があります。各表現が他の表現と一致していることを確認したい。

では、最も基本的な表現を考え出して、お互いの表現を基本的な表現に変えてみませんか?その後、基本的なものを変更するだけで十分であり、ビューは依然として一貫しています。


レイアウトの例:最初の表現は、次のようにしましょう:

panelA(
    panelB(
        panelC(
            widget1
            widget2
        )
        panelD(
            widget3
        )
    )
    widget4
)

したがって、次のタプルのリストである「より単純な」表現に変換します。

[
    (panelA, panelB, panelC, widget1),
    (panelA, panelB, panelC, widget2),
    (panelA, panelB, panelD, widget3),
    (panelA, widget4),
]

次に、このグラフをSwingで使用しながら、上の表現を特殊なツリーに変換するビューを作成します。ツリービューで使用すると、タプルの最後の要素のリストのみを返すビューがあります。

「シンプル」または「ベーシック」とはどういう意味ですか?最も重要なのは、どのビューにも簡単にアクセスできることです(そのため、各ビューの計算が簡単になります)。また、どのビューからでも簡単に変更できる必要があります。

今、レイアウトビューを使用してこの構造を変更したいとしましょう。「panelC.parent = panelD」の呼び出しを変換して、「panelDが含まれているリストを検索し、panelCを含むすべてのリストを検索し、それらのリストのすべての要素を置き換え、panelCの前にある最初のリストの一部をpanelDの前に含める」 。


他の人々が指摘したように-オブザーバーは役に立つかもしれません。

構文解析ツリー/ AST /制御フローグラフについて話している場合、グラフを変更したことをビューに通知する必要はありません。これを使用すると、グラフが検査され、検査によって「基本」表現が動的にビュー表現に変わるためです。

Swingで話している場合、1つのビューへの変更は他のビューで通知される必要があるため、ユーザーが見ることができるものは変更されます。

最後に-これは非常にケース固有の質問です。レイアウトと言語分析に使用する場合、完全なソリューションは大きく異なり、完全に一般的なソリューションは地獄のように醜くて高価になります。


PS。上記の表現は醜く、作成されたアドホックなどです。実際のソリューションではなく、概念を示すことのみを目的としています。


非アドホックな方法でそれをどのように行うのですか?私は完全に一般的なソリューションを意味するのではなく、パターン、戦略、またはこれらの種類の問題を少しややこしくないようにする良い習慣を意味します。
ジオ

1.表示パターンを使用します。実際、VとCが同じものであるMVSに似ています。swingまたはディレクトリ階層のビューであり、モデルは内部記述です。 /内部表現は、適用される操作を覚えておいてください。ビューをできるだけシンプルにする必要があります。これにより、ビューがシンプルになり、場合によってはアトミックになります。覚えておいてください:ビューの実装を簡単にし、ビューからの変更を簡単に導入できる表現が必要です
Filip Malczak '25
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.