Eu sou reconhecidamente tendencioso como alguém que aplica esses conceitos em C ++ pela linguagem e sua natureza, assim como meu domínio e até mesmo pela maneira como usamos a linguagem. Mas, considerando essas coisas, acho projetos imutáveis são o aspecto menos interessante quando se trata de colher grande parte dos benefícios associados à programação funcional, como segurança de threads, facilidade de raciocínio sobre o sistema, encontrar mais reutilização de funções (e descobrir que podemos combiná-los em qualquer ordem, sem surpresas desagradáveis), etc.
Pegue este exemplo simplista de C ++ (reconhecidamente não otimizado para simplificar, para evitar me envergonhar na frente de qualquer especialista em processamento de imagem):
// Inputs an image and outputs a new one with the specified size.
Image resized_image(const Image& src, int new_w, int new_h)
{
Image dst(new_w, new_h);
for (int y=0; y < new_h; ++y)
{
for (int x=0; x < new_w; ++x)
dst[y][x] = src.sample(x / (float)new_w, y / (float)new_h);
}
return dst;
}
Embora a implementação dessa função mude o estado local (e temporário) na forma de duas variáveis de contador e uma imagem local temporária para a saída, ela não tem efeitos colaterais externos. Ele insere uma imagem e gera uma nova. Podemos multithread para o conteúdo de nossos corações. É fácil de raciocinar, fácil de testar completamente. É seguro para exceções, pois, se algo der errado, a nova imagem será descartada automaticamente e não precisaremos nos preocupar em reverter efeitos colaterais externos (não há imagens externas sendo modificadas fora do escopo da função, por assim dizer).
Vejo pouco a ganhar e potencialmente muito a perder, tornando Image
imutável no contexto acima, em C ++, exceto para potencialmente tornar a função acima mais difícil de implementar e possivelmente um pouco menos eficiente.
Pureza
Portanto, funções puras (livres de efeitos colaterais externos ) são muito interessantes para mim, e enfatizo a importância de favorecê-las com frequência para os membros da equipe, mesmo em C ++. Porém, projetos imutáveis, aplicados em geral apenas em contextos e nuances ausentes, não são tão interessantes para mim, pois, dada a natureza imperativa da linguagem, muitas vezes é útil e prático poder modificar alguns objetos temporários locais no processo de maneira eficiente (ambos para desenvolvedor e hardware) implementando uma função pura.
Cópia barata de estruturas robustas
A segunda propriedade mais útil que encontro é a capacidade de copiar, de maneira barata, as estruturas de dados realmente pesadas, quando o custo de fazê-lo, como normalmente seria incorrido para tornar as funções puras, dada sua natureza estrita de entrada / saída, não seria trivial. Não seriam pequenas estruturas que podem caber na pilha. Seriam estruturas grandes e pesadas, como toda aScene
de um videogame.
Nesse caso, a sobrecarga de cópia pode impedir oportunidades de um paralelismo efetivo, porque pode ser difícil paralelizar a física e renderizar efetivamente sem travar e gargalhar uma à outra se a física estiver mudando a cena que o renderizador está tentando desenhar simultaneamente e, ao mesmo tempo, tendo uma profunda profundidade física. copiar toda a cena do jogo apenas para produzir um quadro com a física aplicada pode ser igualmente ineficaz. No entanto, se o sistema de física fosse "puro" no sentido de que apenas introduziu uma cena e produziu uma nova com a física aplicada, e essa pureza não custou a cópia astronômica aérea, ele poderia operar com segurança em paralelo com a física. renderizador sem um esperando pelo outro.
Portanto, a capacidade de copiar de maneira barata os dados realmente pesados do estado do seu aplicativo e gerar novas versões modificadas com custo mínimo para processamento e uso da memória pode realmente abrir novas portas para a pureza e o paralelismo eficaz, e aí encontro muitas lições para aprender de como as estruturas de dados persistentes são implementadas. Mas tudo o que criamos usando essas lições não precisa ser totalmente persistente ou oferecer interfaces imutáveis (ele pode usar a cópia na gravação, por exemplo, ou um "construtor / transitório"), para atingir essa capacidade de ser muito barato copiar e modificar apenas seções da cópia sem dobrar o uso e o acesso à memória em nossa busca por paralelismo e pureza em nossas funções / sistemas / pipeline.
Imutabilidade
Finalmente, há a imutabilidade que considero a menos interessante dessas três, mas ela pode impor, com mão de ferro, quando certos designs de objetos não devem ser usados como temporários locais para uma função pura e, em um contexto mais amplo, um valioso tipo de "pureza no nível do objeto", pois em todos os métodos não causa mais efeitos colaterais externos (não altera mais as variáveis de membros fora do escopo local imediato do método).
E embora eu considere o menos interessante desses três em linguagens como C ++, certamente pode simplificar o teste, a segurança de threads e o raciocínio de objetos não triviais. Pode ser uma carga de trabalho trabalhar com a garantia de que um objeto não pode receber nenhuma combinação de estado exclusiva fora do construtor, por exemplo, e que podemos distribuí-lo livremente, mesmo por referência / ponteiro, sem depender da constância e da leitura. apenas iteradores e manipuladores e tal, garantindo (bem, pelo menos o máximo que pudermos no idioma) que seu conteúdo original não será alterado.
Mas acho isso a propriedade menos interessante porque a maioria dos objetos que considero benéficos como sendo usados temporariamente, de forma mutável, para implementar uma função pura (ou mesmo um conceito mais amplo, como um "sistema puro" que pode ser um objeto ou uma série de funciona com o efeito final de simplesmente introduzir algo e produzir algo novo sem tocar em mais nada), e acho que a imutabilidade levada às extremidades em uma linguagem amplamente imperativa é um objetivo bastante contraproducente. Eu o aplicaria com moderação nas partes da base de código em que realmente ajuda mais.
Finalmente:
[...] parece que estruturas de dados persistentes não são suficientes para lidar com cenários em que um thread faz uma alteração que é visível para outros threads. Para isso, parece que devemos usar dispositivos como átomos, referências, memória transacional de software ou mesmo mecanismos clássicos de bloqueio e sincronização.
Naturalmente, se o seu design exigir que modificações (no sentido do design do usuário final) sejam visíveis para vários threads simultaneamente à medida que ocorrem, voltamos à sincronização ou, pelo menos, à prancheta para descobrir algumas maneiras sofisticadas de lidar com isso ( Eu já vi alguns exemplos muito elaborados usados por especialistas que lidam com esse tipo de problema na programação funcional).
Mas eu descobri que, uma vez que você obtém esse tipo de cópia e capacidade de produzir versões parcialmente modificadas de estruturas pesadas, como é o caso das estruturas de dados persistentes como exemplo, muitas vezes abre muitas portas e oportunidades que você pode não pensamos antes em paralelizar código que pode ser executado de forma completamente independente um do outro em um tipo de pipeline paralelo estrito de E / S. Mesmo que algumas partes do algoritmo precisem ser de natureza serial, você pode adiar esse processamento para um único encadeamento, mas descobrir que a inclinação desses conceitos abriu portas para facilitar e sem preocupação paralelizar 90% do trabalho pesado, por exemplo,