Existem alguns bons exemplos aqui, mas eu queria ir com alguns pessoais, onde a imutabilidade ajudou muito. No meu caso, comecei a projetar uma estrutura de dados simultânea imutável, principalmente com a esperança de poder executar código com confiança em paralelo com leituras e gravações sobrepostas e sem ter que me preocupar com as condições da corrida. Houve uma palestra que John Carmack deu que me inspirou a fazê-lo onde ele falou sobre essa ideia. É uma estrutura bastante básica e bastante trivial para implementar assim:
É claro que, com mais alguns sinos e assobios, é possível remover elementos em tempo constante e deixar orifícios recuperáveis para trás e fazer com que os blocos sejam eliminados se ficarem vazios e potencialmente liberados para uma determinada instância imutável. Mas, basicamente, para modificar a estrutura, você modifica uma versão "transitória" e submete atomicamente as alterações feitas para obter uma nova cópia imutável que não toque a antiga, com a nova versão criando apenas novas cópias dos blocos que devem ser tornados únicos, enquanto cópias e referências rasas contam os outros.
No entanto, não achei queútil para fins de multithreading. Afinal, ainda existe o problema conceitual em que, digamos, um sistema de física aplica a física simultaneamente, enquanto um jogador está tentando mover elementos em um mundo. Com qual cópia imutável dos dados transformados, o que o jogador transformou ou o que o sistema físico transformou? Portanto, eu realmente não encontrei uma solução simples e agradável para esse problema conceitual básico, exceto por ter estruturas de dados mutáveis que apenas bloqueiam de maneira mais inteligente e desencorajam leituras e gravações sobrepostas nas mesmas seções do buffer para evitar encadeamentos. Isso é algo que John Carmack parece ter descoberto como resolver em seus jogos; pelo menos ele fala sobre isso como se quase pudesse ver uma solução sem abrir um carro de vermes. Eu não cheguei tão longe quanto ele a esse respeito. Tudo o que vejo são infinitas questões de design se eu tentasse paralelizar tudo ao redor de imutáveis. Eu gostaria de poder passar um dia mexendo com seu cérebro, já que a maioria dos meus esforços começou com as idéias que ele jogou fora.
No entanto, achei enorme valor dessa estrutura de dados imutável em outras áreas. Até o uso agora para armazenar imagens realmente estranhas e que fazem com que o acesso aleatório exija mais algumas instruções (mudança à direita e pouco a pouco and
junto com uma camada de indicação indireta), mas abordarei os benefícios abaixo.
Desfazer sistema
Um dos lugares mais imediatos que encontrei para se beneficiar disso foi o sistema de desfazer. Desfazer o código do sistema costumava ser uma das coisas mais suscetíveis a erros na minha área (indústria de efeitos visuais), e não apenas nos produtos em que trabalhei, mas em produtos concorrentes (os sistemas de desfazer também eram esquisitos) porque havia muitas diferenças tipos de dados com os quais se preocupar em desfazer e refazer corretamente (sistema de propriedades, alterações de dados de malha, alterações de shader que não eram baseadas em propriedades, como trocar um pelo outro, alterações na hierarquia de cenas, como alterar o pai de um filho, alterações de imagem / textura, etc. etc. etc.).
Portanto, a quantidade de código de desfazer necessária era enorme, muitas vezes rivalizando com a quantidade de código que implementa o sistema para o qual o sistema de desfazer teve que registrar alterações de estado. Ao me basear nessa estrutura de dados, eu consegui fazer com que o sistema de desfazer fosse exatamente o seguinte:
on user operation:
copy entire application state to undo entry
perform operation
on undo/redo:
swap application state with undo entry
Normalmente, o código acima seria enormemente ineficiente quando os dados da cena abrangem gigabytes para copiá-los por completo. Mas essa estrutura de dados apenas copia superficialmente coisas que não foram alteradas e, na verdade, tornou barato o suficiente armazenar uma cópia imutável de todo o estado do aplicativo. Portanto, agora eu posso implementar sistemas de desfazer tão facilmente quanto o código acima e focar apenas no uso dessa estrutura de dados imutável para tornar a cópia de partes inalteradas do estado do aplicativo cada vez mais baratas. Desde que comecei a usar essa estrutura de dados, todos os meus projetos pessoais desfazem sistemas apenas usando esse padrão simples.
Agora ainda há alguma sobrecarga aqui. Na última vez em que medi, era de cerca de 10 kilobytes para copiar superficialmente todo o estado do aplicativo sem fazer alterações (isso é independente da complexidade da cena, pois a cena é organizada em uma hierarquia, portanto, se nada abaixo da raiz mudar, apenas a raiz é copiado superficialmente sem ter que descer até as crianças). Isso está longe de 0 bytes, pois seria necessário para um sistema desfazer apenas armazenar deltas. Mas com 10 kilobytes de overhead por operação, ainda é apenas um megabyte por 100 operações de usuário. Além disso, eu ainda poderia esmagar isso ainda mais no futuro, se necessário.
Exceção-Segurança
Exceção de segurança com uma aplicação complexa não é uma questão trivial. No entanto, quando o estado do seu aplicativo é imutável e você está apenas usando objetos transitórios para tentar confirmar transações de alteração atômica, é inerentemente seguro para exceções, pois se qualquer parte do código for lançada, o transitório será descartado antes de fornecer uma nova cópia imutável . Isso banaliza uma das coisas mais difíceis que sempre achei acertadas em uma complexa base de código C ++.
Muitas pessoas costumam usar recursos em conformidade com RAII em C ++ e acham que isso é suficiente para ser seguro contra exceções. Geralmente não é, uma vez que uma função geralmente pode causar efeitos colaterais em estados além daqueles locais em seu escopo. Geralmente, você precisa começar a lidar com proteções de escopo e lógica sofisticada de reversão nesses casos. Essa estrutura de dados fez com que eu normalmente não precisasse me preocupar com isso, pois as funções não estão causando efeitos colaterais. Eles estão retornando cópias imutáveis transformadas do estado do aplicativo em vez de transformar o estado do aplicativo.
Edição não destrutiva
A edição não destrutiva é basicamente operações de estratificação / empilhamento / conexão, sem tocar nos dados do usuário original (apenas dados de entrada e dados de saída sem tocar na entrada). É normalmente trivial implementar com um aplicativo de imagem simples como o Photoshop e pode não se beneficiar muito dessa estrutura de dados, pois muitas operações podem apenas querer transformar todos os pixels da imagem inteira.
No entanto, com a edição de malha não destrutiva, por exemplo, muitas operações geralmente desejam transformar apenas uma parte da malha. Uma operação pode apenas querer mover alguns vértices aqui. Outro pode apenas querer subdividir alguns polígonos lá. Aqui, a estrutura de dados imutável ajuda bastante a evitar a necessidade de fazer uma cópia inteira de toda a malha apenas para retornar uma nova versão da malha com uma pequena parte alterada.
Minimizando efeitos colaterais
Com essas estruturas em mãos, também facilita a gravação de funções que minimizam os efeitos colaterais sem incorrer em uma enorme penalidade de desempenho. Eu me pego escrevendo mais e mais funções que retornam estruturas de dados imutáveis por valor hoje em dia, sem incorrer em efeitos colaterais, mesmo quando isso parece um pouco inútil.
Por exemplo, normalmente a tentação de transformar várias posições pode ser aceitar uma matriz e uma lista de objetos e transformá-los de maneira mutável. Hoje em dia, acabo retornando uma nova lista de objetos.
Quando você tem mais funções como essa em seu sistema que não causam efeitos colaterais, definitivamente torna mais fácil raciocinar sobre a correção e testar sua correção.
Os benefícios de cópias baratas
Enfim, essas são as áreas em que eu achei mais utilizadas as estruturas de dados imutáveis (ou estruturas de dados persistentes). Também fiquei um pouco zeloso inicialmente e criei uma árvore imutável, uma lista vinculada imutável e uma tabela de hash imutável, mas com o tempo raramente encontrei tanto uso para elas. Eu encontrei principalmente o maior uso do contêiner imutável e robusto no diagrama acima.
Eu também ainda tenho muito código trabalhando com mutáveis (considero uma necessidade prática pelo menos para código de baixo nível), mas o estado principal do aplicativo é uma hierarquia imutável, passando de uma cena imutável para componentes imutáveis dentro dela. Alguns dos componentes mais baratos ainda são copiados na íntegra, mas os mais caros, como malhas e imagens, usam a estrutura imutável para permitir cópias parciais e baratas apenas das peças que precisavam ser transformadas.
ConcurrentModificationException
que geralmente é causado pelo mesmo encadeamento que modifica a coleção no mesmo encadeamento, no corpo de umforeach
loop na mesma coleção.