Geralmente, tipos imutáveis criados em linguagens que não giram em torno da imutabilidade tenderão a custar mais tempo para o desenvolvedor criar e potencialmente usar se exigirem algum tipo de objeto "construtor" para expressar as alterações desejadas (isso não significa que o o trabalho será maior, mas há um custo inicial nesses casos). Além disso, independentemente de o idioma facilitar a criação de tipos imutáveis ou não, ele sempre exigirá algum processamento e sobrecarga de memória para tipos de dados não triviais.
Tornando as funções desprovidas de efeitos colaterais
Se você trabalha em linguagens que não giram em torno da imutabilidade, acho que a abordagem pragmática não é a de tornar imutável todo tipo de dados. Uma mentalidade potencialmente muito mais produtiva que oferece muitos dos mesmos benefícios é se concentrar em maximizar o número de funções em seu sistema que causam zero efeitos colaterais .
Como um exemplo simples, se você tem uma função que causa um efeito colateral como este:
// Make 'x' the absolute value of itself.
void make_abs(int& x);
Então não precisamos de um tipo de dados inteiro imutável que proíba operadores como atribuição de pós-inicialização para fazer com que essa função evite efeitos colaterais. Podemos simplesmente fazer isso:
// Returns the absolute value of 'x'.
int abs(int x);
Agora a função não mexe com x
nada fora do seu escopo e, nesse caso trivial, podemos até raspar alguns ciclos, evitando qualquer sobrecarga associada ao indireção / alias. No mínimo, a segunda versão não deve ser mais cara em termos de computação que a primeira.
Coisas caras para copiar na íntegra
É claro que a maioria dos casos não é tão trivial se queremos evitar que uma função cause efeitos colaterais. Um caso de uso complexo do mundo real pode ser mais ou menos assim:
// Transforms the vertices of the specified mesh by
// the specified transformation matrix.
void transform(Mesh& mesh, Matrix4f matrix);
Nesse ponto, a malha pode exigir algumas centenas de megabytes de memória com mais de cem mil polígonos, ainda mais vértices e arestas, vários mapas de textura, destinos de metamorfose etc. Seria muito caro copiar essa malha inteira apenas para fazer isso. transform
função livre de efeitos colaterais, assim:
// Returns a new version of the mesh whose vertices been
// transformed by the specified transformation matrix.
Mesh transform(Mesh mesh, Matrix4f matrix);
E é nesses casos que copiar algo na sua totalidade normalmente seria uma sobrecarga épica, onde eu achei útil transformar Mesh
uma estrutura de dados persistente e um tipo imutável com o "construtor" analógico para criar versões modificadas para que pode simplesmente copiar superficialmente e contar referências de peças que não são únicas. É tudo com o foco de poder escrever funções de malha livres de efeitos colaterais.
Estruturas de dados persistentes
E nesses casos em que copiar tudo é tão incrivelmente caro, achei o esforço de projetar um imutável Mesh
para realmente render, mesmo que tivesse um custo inicial bastante alto , porque não simplificava apenas a segurança do encadeamento. Ele também simplificou a edição não destrutiva (permitindo que o usuário estratifique operações de malha sem modificar sua cópia original), desfaz sistemas (agora o sistema desfazer pode apenas armazenar uma cópia imutável da malha antes das alterações feitas por uma operação sem explodir a memória use) e exceção-safety (agora, se ocorrer uma exceção na função acima, a função não precisará reverter e desfazer todos os efeitos colaterais, pois não causou nenhum).
Nesses casos, posso dizer com confiança que o tempo necessário para tornar essas pesadas estruturas de dados imutáveis economizou mais tempo do que custa, pois comparei os custos de manutenção desses novos designs com os anteriores, que giravam em torno da mutabilidade e das funções que causavam efeitos colaterais, e os projetos mutáveis anteriores custam muito mais tempo e são muito mais propensos a erros humanos, especialmente em áreas que são realmente tentadoras para os desenvolvedores negligenciarem durante o tempo de crise, como segurança de exceção.
Então, acho que tipos de dados imutáveis realmente valem a pena nesses casos, mas nem tudo precisa ser imutável para tornar a maioria das funções em seu sistema livre de efeitos colaterais. Muitas coisas são baratas o suficiente para copiar apenas na íntegra. Além disso, muitos aplicativos do mundo real precisarão causar alguns efeitos colaterais aqui e ali (no mínimo, como salvar um arquivo), mas normalmente existem muito mais funções que podem ser desprovidas de efeitos colaterais.
O ponto de ter alguns tipos de dados imutáveis para mim é garantir que possamos escrever o número máximo de funções para ficar livre de efeitos colaterais, sem incorrer em sobrecarga épica na forma de copiar em profundidade estruturas maciças de dados, à esquerda e à direita na íntegra, quando apenas pequenas porções deles precisam ser modificados. A existência de estruturas de dados persistentes nesses casos acaba se tornando um detalhe de otimização para permitir que escrevamos nossas funções para estar livre de efeitos colaterais, sem pagar um custo épico para fazê-lo.
Sobrecarga imutável
Agora, conceitualmente, as versões mutáveis sempre terão uma vantagem em eficiência. Sempre há essa sobrecarga computacional associada a estruturas de dados imutáveis. Mas achei uma troca digna nos casos que descrevi acima e você pode se concentrar em tornar a sobrecarga suficientemente mínima por natureza. Eu prefiro esse tipo de abordagem em que a correção se torna fácil e a otimização se torna mais difícil do que a otimização sendo mais fácil, mas a correção se torna mais difícil. Não é tão desmoralizante ter um código que funcione perfeitamente corretamente, necessitando de mais ajustes sobre o código que não funcione corretamente em primeiro lugar, não importa a rapidez com que obtém seus resultados incorretos.