Estruturas imutáveis ​​e hierarquia profunda da composição

9

Estou desenvolvendo um aplicativo GUI, trabalhando intensamente com gráficos - você pode pensar nisso como um editor de vetores, por uma questão de exemplo. É muito tentador tornar todas as estruturas de dados imutáveis ​​- para que eu possa desfazer / refazer, copiar / colar e muitas outras coisas quase sem esforço.

Por uma questão de simplicidade, usarei o seguinte exemplo - o aplicativo é usado para editar formas poligonais, por isso tenho o objeto "Polygon", que é simplesmente uma lista de pontos imutáveis:

Scene -> Polygon -> Point

E assim, eu tenho apenas uma variável mutável no meu programa - aquela que contém o objeto Scene atual. O problema que tenho começa quando tento implementar o arrastamento de pontos - na versão mutável, simplesmente pego um Pointobjeto e começo a modificar suas coordenadas. Na versão imutável - eu estou preso. Eu poderia ter armazenado índices Polygonno atual Scene, índice do ponto arrastado Polygone substituí-lo todas as vezes. Mas essa abordagem não é escalável - quando os níveis de composição vão para 5 e mais, o clichê se tornaria insuportável.

Tenho certeza de que esse problema pode ser resolvido - afinal, existe o Haskell com estruturas completamente imutáveis ​​e a mônada de IO. Mas eu simplesmente não consigo encontrar como.

Você pode me dar uma dica?

Rogach
fonte
@ Job - é assim que funciona agora, e me dá muitas dores. Então eu estou procurando abordagens alternativas - e imutabilidade parece perfeito para esta estrutura de aplicação, pelo menos antes de adicionar interação do usuário para ele :)
Rogach
@ Roger: Você pode explicar mais sobre o seu código clichê?
Rwong

Respostas:

9

Eu poderia ter armazenado índices de polígono na cena atual, índice de ponto arrastado em polígono e substituí-lo sempre. Mas essa abordagem não é escalável - quando os níveis de composição vão para 5 e mais, o clichê se tornaria insuportável.

Você está absolutamente certo, essa abordagem não aumenta se você não conseguir contornar o padrão . Especificamente, o clichê para criar uma cena totalmente nova com uma minúscula subparte foi alterado. No entanto, muitas linguagens funcionais fornecem uma construção para lidar com esse tipo de manipulação de estrutura aninhada: lentes.

Uma lente é basicamente um gerador de dados para imutáveis. Uma lente focaliza uma pequena parte de uma estrutura maior. Dada uma lente, há duas coisas que você pode fazer com ela - é possível visualizar a pequena parte de um valor da estrutura maior ou definir a pequena parte de um valor de uma estrutura maior para um novo valor. Por exemplo, suponha que você tenha uma lente focada no terceiro item de uma lista:

thirdItemLens :: Lens [a] a

Esse tipo significa que a estrutura maior é uma lista de itens e a subparte pequena é um deles. Dada essa lente, você pode visualizar e definir o terceiro item da lista:

> view thirdItemLens [1, 2, 3, 4, 5]
3
> set thirdItemLens 100 [1, 2, 3, 4, 5]
[1, 2, 100, 4, 5]

A razão pela qual as lentes são úteis é porque são valores que representam getters e setters, e você pode abstraí-los da mesma maneira que outros valores. Você pode criar funções que retornam lentes, por exemplo, uma listItemLensfunção que pega um número ne retorna uma lente exibindo o nitem em uma lista. Além disso, as lentes podem ser compostas :

> firstLens = listItemLens 0
> thirdLens = listItemLens 2
> firstOfThirdLens = lensCompose firstLens thirdLens
> view firstOfThirdLens [[1, 2], [3, 4], [5, 6], [7, 8]]
5
> set firstOfThirdLens 100 [[1, 2], [3, 4], [5, 6], [7, 8]]
[[1, 2], [3, 4], [100, 6], [7, 8]]

Cada lente encapsula o comportamento para atravessar um nível da estrutura de dados. Ao combiná-los, você pode eliminar o padrão para atravessar vários níveis de estruturas complexas. Por exemplo, supondo que você tenha um scenePolygonLens ique veja o ipolígono em uma cena e um polygonPointLens nque veja o nthponto em um polígono, você pode criar um construtor de lentes para focar apenas o ponto específico de que você gosta em uma cena inteira como:

scenePointLens i n = lensCompose (polygonPointLens n) (scenePolygonLens i)

Agora, suponha que um usuário clique no ponto 3 do polígono 14 e o mova 10 pixels para a direita. Você pode atualizar sua cena da seguinte maneira:

lens = scenePointLens 14 3
point = view lens currentScene
newPoint = movePoint 10 0 point
newScene = set lens newPoint currentScene

Isso contém muito bem todo o padrão para percorrer e atualizar uma cena dentro lens, tudo com o que você precisa se preocupar é com o que deseja mudar o ponto. Você pode abstrair ainda mais isso com uma lensTransformfunção que aceita uma lente, um alvo e uma função para atualizar a visão do alvo através da lente:

lensTransform lens transformFunc target =
  current = view lens target
  new = transformFunc current
  set lens new target

Isso pega uma função e a transforma em um "atualizador" em uma estrutura de dados complicada, aplicando a função apenas à visualização e usando-a para construir uma nova visualização. Voltando ao cenário de mover o terceiro ponto do 14º polígono para os 10 pixels à direita, isso pode ser expresso em termos lensTransformcomo:

lens = scenePointLens 14 3
moveRightTen point = movePoint 10 0 point
newScene = lensTransform lens moveRightTen currentScene

E isso é tudo o que você precisa para atualizar toda a cena. Essa é uma ideia muito poderosa e funciona muito bem quando você tem algumas funções interessantes para construir lentes visualizando as partes de seus dados importantes.

No entanto, tudo isso é bastante interessante atualmente, mesmo na comunidade de programação funcional. É difícil encontrar um bom suporte de biblioteca para trabalhar com lentes e ainda mais difícil explicar como elas funcionam e quais são os benefícios para seus colegas de trabalho. Tome esta abordagem com um grão de sal.

Jack
fonte
Excelente explicação! Agora eu entendo o que são lentes!
Vincent Lecrubier 22/03
13

Eu trabalhei exatamente no mesmo problema (mas apenas com 3 níveis de composição). A idéia básica é clonar e depois modificar . No estilo de programação imutável, a clonagem e a modificação precisam acontecer juntas, o que se torna objeto de comando .

Observe que, no estilo de programação mutável, a clonagem seria necessária de qualquer maneira:

  • Para permitir desfazer / refazer
  • O sistema de exibição pode precisar exibir simultaneamente o modelo "antes da edição" e "durante a edição", sobrepostos (como linhas fantasmas), para que o usuário possa ver as alterações.

No estilo de programação mutável,

  • A estrutura existente é profundamente clonada
  • As alterações são feitas na cópia clonada
  • O mecanismo de exibição é instruído a renderizar a estrutura antiga em linhas fantasmas e a estrutura clonada / modificada em cores.

No estilo de programação imutável,

  • Cada ação do usuário que resulta em modificação de dados é mapeada para uma sequência de "comandos".
  • Um objeto de comando encapsula exatamente qual modificação deve ser aplicada e uma referência à estrutura original.
    • No meu caso, meu objeto de comando lembra apenas o índice de pontos que precisa ser alterado e as novas coordenadas. (ou seja, muito leve, pois não estou seguindo estritamente o estilo imutável.)
  • Quando um objeto de comando é executado, ele cria uma cópia profunda modificada da estrutura, tornando a modificação permanente na nova cópia.
  • À medida que o usuário faz mais edições, mais objetos de comando serão criados.
rwong
fonte
11
Por que fazer uma cópia profunda de uma estrutura de dados imutável? Você só precisa copiar a "lombada" das referências do objeto modificado para a raiz e reter referências às partes restantes da estrutura original.
Reintegrar Monica
3

Objetos profundamente imutáveis ​​têm a vantagem de que a clonagem profunda de algo simplesmente requer a cópia de uma referência. Eles têm a desvantagem de que fazer uma pequena alteração em um objeto profundamente aninhado exige a construção de uma nova instância de cada objeto no qual ele está aninhado. Objetos mutáveis ​​têm a vantagem de alterar um objeto é fácil - basta fazê-lo - mas a clonagem profunda de um objeto requer a construção de um novo objeto que contém um clone profundo de todos os objetos aninhados. Pior ainda, se alguém deseja clonar um objeto e fazer uma alteração, clonar esse objeto, fazer outra alteração, etc., não importa quão grandes ou pequenas sejam as alterações, é necessário manter uma cópia de toda a hierarquia para todas as versões salvas do objeto. estado do objeto. Desagradável.

Uma abordagem que vale a pena considerar seria definir um tipo abstrato "talvezMutável" com tipos de derivativos mutáveis ​​e profundamente imutáveis. Todos esses tipos apresentariam um AsImmutablemétodo; chamar esse método em uma instância profundamente imutável de um objeto simplesmente retornaria essa instância. Invocá-lo em uma instância mutável retornaria uma instância profundamente imutável cujas propriedades eram instantâneos profundamente imutáveis ​​de seus equivalentes no original. Tipos imutáveis ​​com equivalentes mutáveis ​​ostentariam um AsMutablemétodo, que construiria uma instância mutável cujas propriedades correspondessem às do original.

Alterar um objeto aninhado em um objeto profundamente imutável exigiria primeiro substituir o objeto imutável externo por um objeto mutável e, em seguida, substituir a propriedade que contém a coisa a ser alterada por outra mutável etc., mas fazer alterações repetidas no mesmo aspecto do objeto o objeto geral não exigiria a criação de objetos adicionais até que fosse feita uma tentativa de chamar AsImmutableum objeto mutável (o que deixaria os objetos mutáveis ​​mutáveis, mas retornaria objetos imutáveis ​​com os mesmos dados).

Como otimizações simples, mas significativas, cada objeto mutável pode conter uma referência em cache a um objeto do seu tipo imutável associado, e cada tipo imutável deve armazenar em cache seu GetHashCodevalor. Ao chamar AsImmutableum objeto mutável, antes de retornar um novo objeto imutável, verifique se ele corresponde à referência em cache. Nesse caso, retorne a referência em cache (abandonando o novo objeto imutável). Caso contrário, atualize a referência em cache para manter o novo objeto e retorná-lo. Se isso for feito, chamadas repetidas paraAsImmutablesem nenhuma mutação intermediária produzirá as mesmas referências de objeto. Mesmo que não se economize o custo de construção das novas instâncias, evitará o custo de memória de mantê-las. Além disso, as comparações de igualdade entre os objetos imutáveis ​​podem ser bastante aceleradas se, na maioria dos casos, os itens comparados forem iguais a referências ou tiverem códigos de hash diferentes.

supercat
fonte