Eu li "O porquê de Y" de Richard P. Gabriel . É um artigo de fácil leitura sobre o combinador Y, o que é bastante raro. O artigo começa com a definição recursiva da função fatorial:
(letrec ((f (lambda (n)
(if (< n 2) 1 (* n (f (- n 1)))))))
(f 10))
E explica que letrec
pode ser definido com um efeito colateral:
(let ((f #f))
(set! f (lambda (n)
(if (< n 2) 1 (* n (f (- n 1))))))
(f 10))
E o restante do artigo descreve que também é possível definir letrec
com o combinador Y:
(define (Y f)
(let ((g (lambda (h)
(lambda (x)
((f (h h)) x)))))
(g g)))
(let ((f (Y (lambda (fact)
(lambda (n)
(if (< n 2) 1 (* n (fact (- n 1)))))))))
(f 10))
Obviamente, isso é muito mais complicado do que a versão com o efeito colateral. A razão pela qual é benéfico preferir o combinador Y sobre o efeito colateral é dada apenas pela declaração:
É mais fácil argumentar sobre linguagens de programação e programas que não têm efeitos colaterais.
Isso não é explicado mais. Eu tento encontrar uma explicação.
optimization
side-effect
ceving
fonte
fonte
Respostas:
Obviamente, você pode encontrar exemplos de funções puras incrivelmente difíceis de ler que executam os mesmos cálculos que funções com efeitos colaterais muito mais fáceis de ler. Especialmente quando você usa uma transformação mecânica como um combinador Y para chegar a uma solução. Não é isso que significa "mais fácil de raciocinar".
A razão pela qual é mais fácil argumentar sobre funções sem efeitos colaterais é que você só precisa se preocupar com as entradas e saídas. Com os efeitos colaterais, você também precisa se preocupar com quantas vezes as funções são chamadas, em que ordem elas são chamadas, quais dados são criados dentro da função, quais dados são compartilhados e quais dados são copiados. E todas essas informações para quaisquer funções que possam ser chamadas internas para a função que você está chamando, e recursivamente internas para essas funções, e assim por diante.
Esse efeito é muito mais fácil de ver no código de produção com várias camadas do que nas funções de exemplo de brinquedo. Principalmente significa que você pode confiar muito mais na assinatura de tipo de uma função. Você realmente percebe a carga dos efeitos colaterais se você faz uma programação funcional pura por um tempo e depois volta a ela.
fonte
Uma propriedade interessante de linguagens sem efeitos colaterais é que a introdução de paralelismo, simultaneidade ou assincronia não pode alterar o significado do programa. Pode torná-lo mais rápido. Ou pode torná-lo mais lento. Mas não pode estar errado.
Isso torna trivial paralelizar programas automaticamente. Tão trivial, de fato, que você geralmente acaba com muito paralelismo! A equipe do GHC experimentou a paralelização automática. Eles descobriram que mesmo programas simples poderiam ser decompostos em centenas, até milhares de threads. A sobrecarga de todos esses threads sobrecarregará qualquer aceleração potencial em várias ordens de magnitude.
Portanto, para a paralelização automática de programas funcionais, o problema passa a ser "como você agrupa pequenas operações atômicas em tamanhos úteis de partes paralelas", em oposição a programas impuros, onde o problema é "como você divide grandes operações monolíticas em úteis tamanhos de peças paralelas ". O bom disso é que o primeiro pode ser feito heuristicamente (lembre-se: se você errar, a pior coisa que pode acontecer é que o programa seja um pouco mais lento do que poderia ser), enquanto o segundo é equivalente a resolver o problema Problema (no caso geral), e se você errar, seu programa simplesmente trava (se você tiver sorte!) Ou retorna resultados sutilmente errados (no pior caso).
fonte
Os idiomas com efeitos colaterais empregam análise de alias para verificar se um local de memória pode precisar ser recarregado após uma chamada de função. Quão conservadora é essa análise depende da linguagem.
Para C, isso deve ser bastante conservador, pois o idioma não é do tipo seguro.
Para Java e C #, eles não precisam ser tão conservadores por causa de sua maior segurança de tipo.
Ser excessivamente conservador evita otimizações de carga.
Essa análise seria desnecessária (ou trivial, dependendo de como você a vê) em um idioma sem efeitos colaterais.
fonte
Sempre há otimizações para tirar proveito de quaisquer suposições que você der. Reordenar as operações vem à mente.
Um exemplo divertido que vem à mente realmente aparece em algumas linguagens assembly mais antigas. Em particular, o MIPS tinha uma regra de que a instrução após um salto condicional foi executada, independentemente de qual ramificação foi executada. Se você não quis isso, você colocou um NOP após o salto. Isso foi feito devido à maneira como o pipeline do MIPS foi estruturado. Havia uma paralisação natural de 1 ciclo incorporada na execução do salto condicional; portanto, você também pode fazer algo útil com esse ciclo!
Os compiladores geralmente procuram uma operação que precisa ser executada nos dois ramos e a deslizam para esse slot, para obter um pouco mais de desempenho. No entanto, se um compilador não puder fazer isso, mas puder mostrar que não houve efeitos colaterais na operação, o compilador poderá oportunamente colocá-lo nesse local. Assim, em um caminho, o código executaria uma instrução mais rapidamente. No outro caminho, nenhum dano causado.
fonte
"letrec pode ser definido com um efeito colateral ..." Não vejo efeito colateral em sua definição. Sim, ele usa
set!
uma maneira típica de produzir efeitos colaterais no Scheme, mas nesse caso não há efeito colateral - porf
ser puramente local, não pode ser referenciado por nenhuma função que não seja localmente. Portanto, não é um efeito colateral visto em qualquer escopo externo. O que esse código faz é desviar de uma limitação no escopo do Scheme que, por padrão, não permite que uma definição lambda se refira a si mesma.Algumas outras linguagens têm declarações em que uma expressão usada para produzir o valor de uma constante pode se referir à própria constante. Nesse idioma, a definição equivalente exata pode ser usada, mas claramente isso não produz um efeito colateral, pois apenas uma constante é usada. Veja, por exemplo, este programa Haskell equivalente:
(que é avaliado em 120).
Isso claramente não tem efeitos colaterais (como para que uma função no Haskell tenha um efeito colateral, ela deve retornar seu resultado envolto em uma Mônada, mas o tipo retornado aqui é um tipo numérico simples), mas é um código estruturalmente idêntico ao código que você cita.
fonte
let
poderia retornar a função local.É algo que é inerente a muitos de nós que depuramos grandes bases de código, mas é preciso lidar com uma escala grande o suficiente no nível superior por um tempo suficiente para apreciá-lo. É como entender a importância de estar em posição no Poker. Inicialmente, não parece uma vantagem tão útil durar no final de cada turno até que você grave um milhão de mãos e perceba que ganhou muito mais dinheiro em posição do que fora.
Dito isto, discordo da ideia de que uma alteração em uma variável local é um efeito colateral. Do meu ponto de vista, uma função não causa efeitos colaterais se não modificar nada fora do seu escopo, que qualquer coisa que toque e mexa não afetará nada abaixo da pilha de chamadas ou qualquer memória ou recurso que a função não tenha adquirido por si mesma .
Em geral, a coisa mais difícil de se raciocinar em uma base de código complexa e de larga escala que não possui o procedimento de teste mais perfeito possível é o gerenciamento de estado persistente, como todas as alterações em objetos granulares no mundo dos videogames enquanto você percorre dezenas de milhares de funções, enquanto tentavam restringir-se a uma lista interminável de suspeitos, que realmente causou a violação de uma invariante em todo o sistema ("isso nunca deveria acontecer, quem o fez?"). Se nada for alterado fora de uma função, isso não poderá causar um mau funcionamento central.
Claro que isso não é possível em todos os casos. Qualquer aplicativo que atualize um banco de dados armazenado em uma máquina diferente é, por natureza, projetado para causar efeitos colaterais, bem como qualquer aplicativo projetado para carregar e gravar arquivos. Mas há muito mais que podemos fazer sem efeitos colaterais em muitas funções e muitos programas se, por exemplo, tivermos uma rica biblioteca de estruturas de dados imutáveis e adotamos ainda mais essa mentalidade.
Curiosamente, John Carmack saltou no LISP e na onda da imutabilidade, apesar de ter começado nos dias da codificação C mais micro-sintonizada. Eu me vi fazendo uma coisa semelhante, embora ainda use muito C. Essa é a natureza dos pragmáticos, acho, que passaram anos depurando e tentando raciocinar sobre sistemas complexos e de larga escala como um todo, de um nível superior. Mesmo os que são surpreendentemente robustos e desprovidos de uma grande quantidade de bugs ainda podem deixá-lo com uma sensação desconfortável de que algo de errado está à espreita ao virar da esquina se houver muito estado persistente complexo sendo modificado no gráfico interconectado mais complexo de chamadas de função entre os milhões de linhas de código. Mesmo que todas as interfaces sejam testadas com um teste de unidade e todas sejam aprovadas,
Na prática, muitas vezes acho que a programação funcional dificulta a compreensão de uma função. Ele ainda gira meu cérebro em reviravoltas e nós, especialmente com lógica recursiva complexa. Mas toda a sobrecarga intelectual associada à descoberta de algumas funções escritas em uma linguagem funcional é diminuída pelo sistema complexo, com estados persistentes sendo alterados em dezenas de milhares de funções, onde cada função que causa efeitos colaterais se soma ao total complexidade do raciocínio sobre a correção de todo o sistema.
Observe que você não precisa de uma linguagem funcional pura para fazer com que as funções evitem efeitos colaterais. Os estados locais alterados em uma função não contam como efeito colateral, como uma
for
variável de contador de loop local para uma função não conta como efeito colateral. Até escrevo código C hoje em dia com o objetivo de evitar efeitos colaterais, quando possível, e criei uma biblioteca de estruturas de dados imutáveis e seguras para threads que podem ser parcialmente modificadas enquanto o restante dos dados é copiado superficialmente, e isso me ajudou a muito a raciocinar sobre a correção do meu sistema. Discordo totalmente do autor nesse sentido. Pelo menos em C e C ++ em software de missão crítica, uma função pode estar documentando como sem efeitos colaterais se não tocar em nada que possa afetar o mundo fora da função.fonte