Na programação funcional, as variáveis ​​locais mutáveis ​​sem efeitos colaterais ainda são consideradas “más práticas”?

23

Ter variáveis ​​locais mutáveis ​​em uma função que é usada apenas internamente (por exemplo, a função não tem efeitos colaterais, pelo menos não intencionalmente) ainda é considerado "não funcional"?

por exemplo, na verificação do estilo do curso "Programação funcional com Scala" considera qualquer var uso ruim

Minha pergunta, se a função não tiver efeitos colaterais, a escrita de código de estilo imperativo ainda é desencorajada?

por exemplo, em vez de usar recursão de cauda com o padrão de acumulador, o que há de errado em criar um loop for local e criar um mutável localListBuffer e adicioná-lo, desde que a entrada não seja alterada?

Se a resposta for "sim, eles são sempre desencorajados, mesmo que não haja efeitos colaterais", qual é o motivo?

Eran Medan
fonte
3
Todos os conselhos, exortações etc. sobre o tópico que eu já ouvi referem-se ao estado mutável compartilhado como fonte de complexidade. Esse curso deve ser consumido apenas por iniciantes? Então é provavelmente uma simplificação excessiva e deliberada bem-intencionada.
precisa saber é o seguinte
3
@KilianFoth: O estado mutável compartilhado é um problema em contextos multithread, mas o estado mutável não compartilhado pode levar os programas a serem difíceis de raciocinar também.
Michael Shaw
1
Eu acho que o uso de uma variável local mutável não é necessariamente uma prática ruim, mas não é um "estilo funcional": acho que o objetivo do curso Scala (que fiz no último outono) é ensinar a programação em um estilo funcional. Depois de distinguir claramente entre o estilo funcional e o imperativo, você pode decidir quando usar qual (caso sua linguagem de programação permita ambos). varé sempre não funcional. O Scala possui valores preguiçosos e otimização da recursão da cauda, ​​o que permite evitar completamente os vars.
Giorgio

Respostas:

17

A única coisa que é inequivocamente uma prática ruim aqui é alegar que algo é uma função pura quando não é.

Se variáveis ​​mutáveis ​​são usadas de maneira verdadeira e completamente independente, a função é externamente pura e todos ficam felizes. Haskell, de fato, suporta isso explicitamente , com o sistema de tipos até garantindo que referências mutáveis ​​não possam ser usadas fora da função que as cria.

Dito isto, acho que falar sobre "efeitos colaterais" não é a melhor maneira de encará-lo (e é por isso que disse "puro" acima). Qualquer coisa que crie uma dependência entre a função e o estado externo torna as coisas mais difíceis de raciocinar, e isso inclui coisas como saber a hora atual ou usar o estado mutável oculto de uma maneira que não seja segura para threads.

CA McCann
fonte
16

O problema não é a mutabilidade em si, é uma falta de transparência referencial.

Uma coisa referencialmente transparente e uma referência a ela sempre devem ser iguais; portanto, uma função referencialmente transparente sempre retornará os mesmos resultados para um determinado conjunto de entradas e uma "variável" referencialmente transparente é realmente um valor e não uma variável, pois não pode mudar. Você pode criar uma função referencialmente transparente que possui uma variável mutável dentro; isso não é um problema. Pode ser mais difícil garantir que a função seja referencialmente transparente, dependendo do que você estiver fazendo.

Há um exemplo em que posso pensar em que a mutabilidade deve ser usada para fazer algo que é muito funcional: memorização. Memoização é o armazenamento em cache de valores de uma função, para que eles não precisem ser recalculados; é referencialmente transparente, mas usa mutação.

Mas, em geral, transparência referencial e imutabilidade andam juntas, exceto uma variável local mutável em uma função e memoização referencialmente transparente, não tenho certeza de que haja outros exemplos em que esse não seja o caso.

Michael Shaw
fonte
4
Seu ponto de vista sobre memorização é muito bom. Observe que Haskell enfatiza fortemente a transparência referencial para programação, mas o comportamento semelhante à memoização da avaliação lenta envolve uma quantidade impressionante de mutação sendo feita pelo tempo de execução da linguagem nos bastidores.
CA McCann
@ CA McCann: Eu acho que o que você diz é muito importante: em uma linguagem funcional, o tempo de execução pode usar a mutação para otimizar a computação, mas não há nenhuma construção na linguagem que permita ao programador usar a mutação. Outro exemplo é um loop while com uma variável de loop: em Haskell, você pode escrever uma função recursiva de cauda que pode ser implementada com uma variável mutável (para evitar o uso da pilha), mas o que o programador vê são argumentos de função imutáveis ​​que são passados ​​de um ligue para o próximo.
Giorgio
@ Michael Shaw: +1 em "O problema não é mutabilidade em si, é uma falta de transparência referencial". Talvez você possa citar a linguagem Clean na qual você tem tipos de exclusividade: eles permitem mutabilidade, mas ainda garantem transparência referencial.
Giorgio
@Giorgio: Eu realmente não sei nada sobre o Clean, embora eu tenha ouvido falar de vez em quando. Talvez eu deva investigar.
Michael Shaw
@ Michael Shaw: Eu não sei muito sobre o Clean, mas sei que ele usa tipos de exclusividade para garantir a transparência referencial. Basicamente, você pode modificar um objeto de dados, desde que, após a modificação, você não tenha mais referências ao valor antigo. Na IMO, isso ilustra seu argumento: a transparência referencial é o ponto mais importante e a imutabilidade é apenas uma maneira possível de garantir isso.
Giorgio
8

Não é realmente bom resumir isso em "boas práticas" versus "más práticas". O Scala suporta valores mutáveis ​​porque eles resolvem certos problemas muito melhor do que valores imutáveis, nomeadamente aqueles que são de natureza iterativa.

Para perspectiva, tenho CanBuildFromquase certeza de que, através de quase todas as estruturas imutáveis ​​fornecidas pelo scala, ocorre algum tipo de mutação internamente. O ponto é que o que eles expõem é imutável. Manter o maior número possível de valores imutáveis ​​ajuda a tornar o programa mais fácil de raciocinar e menos propenso a erros .

Isso não significa que você necessariamente precise evitar estruturas e valores mutáveis ​​internamente quando tiver um problema mais adequado à mutabilidade.

Com isso em mente, muitos problemas que normalmente requerem variáveis ​​mutáveis ​​(como loop) podem ser resolvidos melhor com muitas das funções de ordem superior fornecidas por linguagens como Scala (map / filter / fold). Esteja ciente disso.

KChaloux
fonte
2
Sim, quase nunca preciso de um loop for ao usar as coleções do Scala. map, filter, foldLeftE forEach fazer o truque na maioria das vezes, mas não quando o fazem, ser capaz de sentir que eu sou "OK" para reverter para código imperativo força bruta é bom. (contanto que não existem efeitos secundários, é claro)
Erã Medan
3

Além de possíveis problemas com a segurança do encadeamento, você também perde muito tipo de segurança. Loops imperativos têm um tipo de retorno Unite podem receber praticamente qualquer expressão para entradas. Funções de ordem superior e até recursão têm tipos e semânticas muito mais precisas.

Você também tem muito mais opções para processamento funcional de contêiner do que com loops imperativos. Com imperativo, você tem basicamente for, whilee pequenas variações sobre aqueles dois como do...whilee foreach.

Em funcional, você tem agregado, contagem, filtro, localização, flatMap, dobra, groupBy, lastIndexWhere, mapa, maxBy, minBy, partição, varredura, varredura, sortBy, sortWith, span e takeWhile, apenas para citar alguns exemplos mais comuns do Scala. biblioteca padrão. Quando você se acostuma a ter esses disponíveis, os forloops imperativos parecem muito básicos em comparação.

A única razão real para usar a mutabilidade local é muito ocasionalmente para o desempenho.

Karl Bielefeldt
fonte
2

Eu diria que é principalmente ok. Além disso, gerar estruturas dessa maneira pode ser uma boa maneira de melhorar o desempenho em alguns casos. Clojure resolveu esse problema fornecendo estruturas de dados temporárias .

A idéia básica é permitir mutações locais em um escopo limitado e congelar a estrutura antes de devolvê-la. Dessa forma, seu usuário ainda pode raciocinar sobre seu código como se fosse puro, mas você pode executar transformações no local quando necessário.

Como o link diz:

Se uma árvore cai na floresta, ela produz um som? Se uma função pura modifica alguns dados locais para produzir um valor de retorno imutável, tudo bem?

Simon Bergot
fonte
2

Não ter nenhuma variável local mutável tem uma vantagem - torna a função mais amigável em relação aos threads.

Fui queimado por uma variável local (não no meu código, nem tinha a fonte), causando uma corrupção de dados de baixa probabilidade. A segurança do encadeamento não foi mencionada de uma forma ou de outra, não houve estado que persistisse nas chamadas e não houve efeitos colaterais. Não me ocorreu que ele pode não ser seguro para threads, perseguir uma corrupção de dados aleatória de 1 em 100.000 é uma dor real.

Loren Pechtel
fonte