Como mantemos as estruturas de dados dependentes atualizadas?


8

Suponha que você tenha uma árvore de análise, uma árvore de sintaxe abstrata e um gráfico de fluxo de controle, cada um deles logicamente derivado do anterior. Em princípio, é fácil construir cada gráfico, dada a árvore de análise, mas como podemos gerenciar a complexidade de atualizar os gráficos quando a árvore de análise é modificada? Sabemos exatamente como a árvore foi modificada, mas como a mudança pode ser propagada para as outras árvores de uma maneira que não se torna difícil de gerenciar?

Naturalmente, o gráfico dependente pode ser atualizado simplesmente reconstruindo-o do zero toda vez que o primeiro gráfico for alterado, mas não haveria como saber os detalhes das alterações no gráfico dependente.

Atualmente, tenho quatro maneiras de tentar resolver esse problema, mas cada uma delas tem dificuldades.

  1. Cada um dos nós da árvore dependente observa os nós relevantes da árvore original, atualizando a si mesmos e as listas de observadores dos nós da árvore original, conforme necessário. A complexidade conceitual disso pode se tornar assustadora.
  2. Cada nó da árvore original possui uma lista dos nós da árvore dependente que dependem especificamente dela e, quando o nó é alterado, define um sinalizador nos nós dependentes para marcá-los como sujos, incluindo os pais dos nós dependentes até o fim para a raiz. Após cada alteração, executamos um algoritmo que é muito parecido com o algoritmo para construir o gráfico dependente do zero, mas pula qualquer nó limpo e reconstrói cada nó sujo, mantendo o controle de se o nó reconstruído é realmente diferente do nó sujo. Isso também pode ser complicado.
  3. Podemos representar a conexão lógica entre o gráfico original e o gráfico dependente como uma estrutura de dados, como uma lista de restrições, talvez projetada usando uma linguagem declarativa. Quando o gráfico original muda, precisamos apenas varrer a lista para descobrir quais restrições são violadas e como a árvore dependente precisa ser alterada para corrigir a violação, todas codificadas como dados.
  4. Podemos reconstruir o gráfico dependente do zero como se não houvesse um gráfico dependente existente e, em seguida, comparar o gráfico existente e o novo gráfico para descobrir como ele foi alterado. Tenho certeza de que essa é a maneira mais fácil, porque sei que existem algoritmos disponíveis para detectar diferenças, mas todos são bastante computacionais e, em princípio, parece desnecessário, por isso evito deliberadamente essa opção.

Qual é a maneira correta de lidar com esse tipo de problema? Certamente deve haver um padrão de design que torne tudo isso quase fácil. Seria bom ter uma boa solução para todos os problemas desta descrição geral. Essa classe de problemas tem um nome?


Deixe-me elaborar os problemas que esse problema causa. Esse problema aparece em vários lugares, sempre que duas partes de um projeto operam em gráficos, com cada gráfico sendo uma representação diferente da mesma coisa que muda enquanto o software está em execução. É como fazer um adaptador para uma interface, mas em vez de agrupar um único objeto ou um número fixo de objetos, precisamos agrupar um gráfico inteiro de tamanho arbitrário.

Toda vez que tento isso, acabo com uma confusão confusa e inatingível. Pode ser difícil acompanhar o fluxo de controle dos observadores quando se torna complicado, e o algoritmo para converter um gráfico em outro é geralmente bastante complicado de seguir quando é definido claramente e não se espalha por várias classes. O problema é que parece não haver maneira de usar apenas um algoritmo de conversão de gráfico simples e direto quando o gráfico original está sendo alterado.

Naturalmente, não podemos simplesmente usar um algoritmo comum de conversão de gráficos diretamente, porque isso não pode responder a mudanças de nenhuma outra maneira que não seja começar do zero, então quais são as alternativas? Talvez o algoritmo possa ser escrito em um estilo de passagem contínua, onde cada etapa do algoritmo é representada como um objeto com um método para cada tipo de nó no gráfico original, como um visitante. Em seguida, o algoritmo pode ser montado compondo vários visitantes simples juntos.


Outro exemplo: suponha que você tenha uma GUI projetada como no Java Swing, usando JPanels e gerenciadores de layout. Você pode simplificar esse processo usando JPanels aninhados no lugar de gerenciadores de layout complexos, para terminar com uma árvore de vários contêineres que inclui nós que existem apenas para fins de layout e, de outra forma, não têm sentido. Agora, suponha que a mesma árvore usada para gerar sua GUI também seja usada em outra parte do seu aplicativo, mas em vez de distribuir a árvore graficamente, ela está trabalhando com uma biblioteca que gerará uma árvore de representação abstrata como um sistema de pastas. Para usar esta biblioteca, precisamos ter uma versão da árvore que não tenha os nós de layout; os nós de layout precisam ser achatados em seus nós pais,


Outra maneira de analisar: o próprio conceito de trabalhar com árvores mutáveis ​​viola a Lei de Deméter . Realmente não seria uma violação da lei se a árvore tivesse um valor como as árvores de análise e as árvores de sintaxe normalmente são, mas nesse caso não haveria nenhum problema, pois nada precisaria ser atualizado. Portanto, esse problema existe como resultado direto da violação da Lei de Demeter, mas como você evita isso em geral quando seu domínio parece ser sobre a manipulação de árvores ou gráficos?

O padrão Composite é uma ferramenta maravilhosa para transformar um gráfico em um único objeto e obedecer à Lei de Demeter. É possível usar o padrão Composite para transformar efetivamente um tipo de árvore em outro? Você pode criar uma árvore de análise composta para que ela funcione como uma árvore de sintaxe abstrata e até mesmo um gráfico de fluxo de controle? Existe uma maneira de fazer isso sem violar o princípio da responsabilidade única ? O padrão Composite tende a fazer com que as classes absorvam todas as responsabilidades que exercem, mas talvez possa ser combinado com o padrão Strategy de alguma forma.


1
Talvez olhar para algoritmos de análise incrementais, por exemplo cstheory.stackexchange.com/questions/6852/...
psr

Respostas:


5

Eu acho que seus cenários estão discutindo variações no Padrão do Observador . Cada nó original (" sujeito ") possui (pelo menos) os dois métodos a seguir:

  • registerObserver(observer) - adiciona um nó dependente à lista de observadores.
  • notifyObservers()- chama x.notify(this)cada observador

E cada nó dependente (" observador ") tem um notify(original)método. Comparando seus cenários:

  1. O notifymétodo recria imediatamente uma subárvore dependente.
  2. O notifymétodo define um sinalizador, a recomputação ocorre após cada conjunto de atualizações.
  3. O notifyObserversmétodo é inteligente e só notifica os observadores cujas restrições são invalidadas. Provavelmente, isso usaria o Padrão do Visitante , para que os nós dependentes possam oferecer um método que decide isso.
  4. (esse padrão não tem relação com a reconstrução da força bruta)

Como as três primeiras idéias são apenas variações no padrão do observador, seu design terá complexidade semelhante (por acaso, elas são realmente ordenadas em complexidade crescente - eu acho que №1 é o mais simples de implementar).

Eu posso pensar de uma melhoria: a construção de árvores dependentes preguiçosamente . Cada nó dependente teria um sinalizador booleano definido como validou invalid. Cada método acessador deve verificar esse sinalizador e, se necessário, recalcular a subárvore. A diferença para №2 é que o recálculo ocorre no acesso, e não na alteração. Provavelmente, isso resultaria em menos cálculos, mas pode levar a dificuldades significativas se o tipo de nó precisar mudar no acesso.


Eu também gostaria de desafiar a necessidade de várias árvores dependentes. Por exemplo, eu sempre estruturo meus analisadores de uma maneira que eles emitem imediatamente um AST. As informações relevantes apenas durante a construção dessa árvore não precisam ser armazenadas em nenhuma estrutura de dados permanente. Da mesma forma, você também pode escolher seus objetos de forma que o AST tenha uma interpretação como um gráfico de fluxo de controle.

Para um exemplo da vida real, a parte do compilador dentro do perlintérprete faz o seguinte: O AST é construído de baixo para cima, durante o qual alguns nós são dobrados constantemente. Em uma segunda execução, os nós são conectados em ordem de execução, durante a qual alguns nós são ignorados por otimizações. O resultado é uma análise muito rápida (e poucas alocações), mas otimizações muito limitadas. Deve-se notar que, embora esse projeto seja possível , provavelmente não é algo pelo qual você deve se esforçar: É umtrade-off calculado violação total do princípio de responsabilidade única .

Se você realmente precisa de várias árvores, também deve considerar se elas realmente precisam ser construídas simultaneamente. Na maioria dos casos, uma árvore de análise é constante após a análise. Da mesma forma, um AST provavelmente permanecerá constante depois que as macros forem resolvidas e as otimizações no nível do AST forem executadas.


Na mesma linha, você pode tentar a Programação Reativa Funcional. Isso pode ser mais flexível: lampwww.epfl.ch/~imaier/pub/DeprecatingObserversTR2010.pdf
Jim Barrows

2

Parece que você está pensando em um caso geral de 2 gráficos, onde o segundo pode ser totalmente derivado do primeiro, e deseja recalcular com eficiência o segundo gráfico quando uma parte da primeira é alterada.

Isso não parece conceitualmente diferente do que o problema de minimizar a recálculo apenas no primeiro gráfico, embora suponha que, quando implementados em um sistema específico, eles provavelmente sejam tipos diferentes em cada gráfico.

É basicamente tudo sobre rastreamento de dependências, dentro e entre gráficos. Para cada nó alterado, atualize todos os dependentes, recursivamente.

Obviamente, antes de fazer qualquer atualização, você deseja classificar topologicamente seu gráfico de dependência. Isso permite que você saiba se você possui dependências circulares criando uma onda potencialmente infinita de atualizações e também assegura que, para qualquer nó, você atualize todos os seus dependentes antes de atualizar esse nó, evitando assim o cálculo inútil que terá que ser refeito mais tarde.

Você não precisa expressar particularmente as dependências em uma linguagem declarativa, mas pode, isso é uma questão totalmente independente.

Esse é um algoritmo geral e, em casos específicos, pode ser que você possa fazer mais para acelerar o processo. Pode ser que parte do trabalho que você esteja realizando para atualizar uma dependência também seja útil na atualização de outras dependências, e um bom algoritmo tiraria vantagem disso.

Na medida em que o algoritmo de conversão de gráficos é uma bagunça impossível de manter, a solução é um pouco específica da linguagem, mas uma abordagem orientada a objetos pode ter algumas classes que lidam puramente com a atualização de dependências em geral - representando dependências, fazendo uma classificação topológica e acionando cálculos . Para fazer o cálculo, eles delegam para suas classes reais, possivelmente usando uma função de primeira classe que foram entregues quando foram criadas, possivelmente porque as classes entregues devem implementar uma interface (como de costume, se não puderem, porque, por Por exemplo, você não os criou, pode usar um adaptador). Suponho que, em alguns casos, você possa usar a reflexão para reunir as informações do gráfico das relações de objeto e chamar os métodos dessa maneira, se for mais fácil de configurar e você não


1

Você mencionou que sabe exatamente como a árvore foi modificada, você saberia quando?

Que tal experimentar HashTrees ou cadeias de hash ( Merkle Tree ) ou geralmente o conceito de detecção de erros . Se as árvores forem enormes, você pode dividir, digamos, o primeiro gráfico em zonas N / 2 ou N raiz e atribuir hashes / somas de verificação a essas zonas. As árvores dependentes manteriam seu próprio conjunto de zonas N / 2 ou N raiz, que são dependentes das zonas das primeiras árvores. Quando mudanças são detectadas na primeira árvore, atualize os nós correspondentes na árvore dependente usando uma pesquisa simples (como você sabe o que mudou e, posteriormente, a soma de verificação / hash para essa zona).


3
Não consigo entender como isso deve funcionar. Como tenho a árvore original e a árvore modificada para fazer comparações diretas, não entendo como a computação em hashes é útil.
Geo

A idéia da detecção de erros é detectar o que mudou e, para seus propósitos, saber onde mudar e, portanto, gerenciar essa mudança (que foi sua pergunta). A sugestão acima é um experimento mental, se suas árvores forem simples o suficiente e tiverem uma propriedade trivial que possa expor o "o que mudou", então você provavelmente não precisará calcular hashes. O mecanismo "detecção de erros", também conhecido como "detecção de alterações", pode ajudar a gerenciar a propagação.
ensolarado

1

Outra representação do problema - você tem alguns dados (gráfico) e diferentes representações (por exemplo, painéis de layout / exibição em árvore). Você quer ter certeza de que cada representação é consistente com outras representações.

Então, por que você não tenta apresentar a representação mais básica e transformar uma a outra para visualizar a representação básica? Basta alterar o básico, e as visualizações ainda serão consistentes.


No exemplo de layout: A primeira representação é, digamos:

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

Então, você o transforma em representação "mais simples", lista das seguintes tuplas:

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

Em seguida, ao usar este gráfico com o Swing, você cria uma exibição que transforma a representação acima em árvore especializada e, ao usar com a exibição em árvore, você tem uma exibição que retorna para você apenas a lista dos últimos elementos da tupla.

O que significa "simples" ou "básico"? Mais importante - deve ser fácil recorrer a qualquer visualização (para que calcular cada visualização seja barato). Além disso, deve ser fácil modificar de qualquer visualização.

Digamos que agora queremos modificar essa estrutura usando a exibição de layout. Ele deve traduzir a chamada "panelC.parent = panelD" para "encontrar qualquer lista que contenha panelD, encontrar todas as listas que contenham panelC, substituir todos os elementos dessa lista, que precedem o panelC por parte da primeira lista antes do panelD" .


Como outras pessoas apontaram - o observador pode ser útil.

Se estivermos falando sobre gráficos de árvores de análise / AST / controle, não precisamos notificar nenhuma visão que o gráfico tenha mudado, porque ao usá-lo, você o inspecionará e a inspeção ativará dinamicamente a representação "básica" para visualizar a representação.

Se estivermos conversando no Swing, a alteração para uma visualização deve ser notificada em outras visualizações, para que as coisas que o usuário possa ver mudem.

No final - esta é uma questão muito específica para cada caso. Eu diria que a solução completa será muito diferente quando você a usar no layout e na análise da linguagem, e a solução totalmente genérica será feia e cara como o inferno.


PS. A representação acima é feia, criada ad-hoc, etc. Destina-se apenas a mostrar o conceito, não a solução da vida real.


Como alguém faz isso de maneira não ad hoc? Não quero dizer uma solução totalmente genérica, apenas um padrão, estratégia ou boas práticas que tornam esses tipos de problemas um pouco menos complicados.
Geo

1. Use o padrão de exibição. Na verdade, mais parecido com o MVS, em que V e C são as mesmas coisas - visualizações para swing ou para hierarquia de diretórios, e modelo é descirpção interna 2. Use o padrão Observer, se necessário (como afirmei - nem sempre é necessário) 3. Ao projetar o modelo / representação interna, lembre-se de quais operações serão aplicadas. Você precisa que seja o mais simples possível, o que tornará as visualizações simples e possivelmente até atômicas. Lembre-se: você precisa de representação que vai fazer vista fácil de implementar e mudanças de viws fácil introduzir
Filip Malczak
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.