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 Point
objeto e começo a modificar suas coordenadas. Na versão imutável - eu estou preso. Eu poderia ter armazenado índices Polygon
no atual Scene
, índice do ponto arrastado Polygon
e 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?
fonte
Respostas:
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:
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:
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
listItemLens
função que pega um númeron
e retorna uma lente exibindo on
item em uma lista. Além disso, as lentes podem ser compostas :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 i
que veja oi
polígono em uma cena e umpolygonPointLens n
que veja onth
ponto 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: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:
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 umalensTransform
função que aceita uma lente, um alvo e uma função para atualizar a visão do alvo através da lente: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
lensTransform
como: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.
fonte
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:
No estilo de programação mutável,
No estilo de programação imutável,
fonte
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
AsImmutable
mé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 umAsMutable
mé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
AsImmutable
um 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
GetHashCode
valor. Ao chamarAsImmutable
um 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 paraAsImmutable
sem 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.fonte