A programação funcional no Scala explica o impacto de um efeito colateral na quebra da transparência referencial:
efeito colateral, o que implica alguma violação da transparência referencial.
Eu li parte do SICP , que discute o uso do "modelo de substituição" para avaliar um programa.
Como eu aproximadamente entender o modelo de substituição com transparência referencial (TR), você pode de-compor uma função em suas partes mais simples. Se a expressão for RT, você poderá descompor a expressão e sempre obter o mesmo resultado.
No entanto, como a citação acima afirma, o uso de efeitos colaterais pode / irá quebrar o modelo de substituição.
Exemplo:
val x = foo(50) + bar(10)
Se foo
e bar
não tiver efeitos colaterais, a execução de qualquer função sempre retornará o mesmo resultado para x
. Mas, se tiverem efeitos colaterais, eles alterarão uma variável que interrompe / lança uma chave inglesa no modelo de substituição.
Sinto-me à vontade com essa explicação, mas não a entendo totalmente.
Por favor, corrija-me e preencha todos os buracos com relação aos efeitos colaterais que causam a RT, discutindo os efeitos no modelo de substituição também.
fonte
RT
desabilita o uso dosubstitution model.
O grande problema de não poder usar osubstitution model
é o poder de usá-lo para raciocinar sobre um programa?Imagine que você está tentando construir uma parede e recebeu uma variedade de caixas em diferentes tamanhos e formas. Você precisa preencher um buraco específico em forma de L na parede; você deve procurar uma caixa em forma de L ou pode substituir duas caixas retas do tamanho apropriado?
No mundo funcional, a resposta é que qualquer uma das soluções funcionará. Ao construir seu mundo funcional, você nunca precisa abrir as caixas para ver o que está dentro.
No mundo imperativo, é perigoso construir sua parede sem inspecionar o conteúdo de cada caixa e compará-las com o conteúdo de todas as outras caixas:
Acho que vou parar antes de desperdiçar seu tempo com metáforas mais improváveis, mas espero que o argumento seja acertado; tijolos funcionais não contêm surpresas ocultas e são totalmente previsíveis. Como você sempre pode usar blocos menores do tamanho e formato certos para substituir um maior e não há diferença entre duas caixas do mesmo tamanho e formato, você tem transparência referencial. Com tijolos imperativos, não basta ter algo do tamanho e formato certos - você precisa saber como o tijolo foi construído. Não é referencialmente transparente.
Em uma linguagem funcional pura, tudo que você precisa ver é a assinatura de uma função para saber o que ela faz. Claro, você pode querer olhar para dentro para ver o desempenho, mas não precisa olhar.
Em uma linguagem imperativa, você nunca sabe o que as surpresas podem esconder por dentro.
fonte
(a, b) -> a
pode ser apenas afst
função e que uma função do tipoa -> a
pode ser apenas aidentity
função, mas você não pode necessariamente dizer algo sobre uma função do tipo(a, a) -> a
, por exemplo.Sim, a intuição está certa. Aqui estão algumas dicas para ser mais preciso:
Como você disse, qualquer expressão de RT deve ter um
single
"resultado". Ou seja, dada umafactorial(5)
expressão no programa, ele sempre deve produzir o mesmo "resultado". Portanto, se um determinadofactorial(5)
está no programa e gera 120, deve sempre gerar 120, independentemente de qual "ordem de etapas" é expandida / calculada - independentemente do tempo .Exemplo: a
factorial
funçãoExistem algumas considerações com esta explicação.
Antes de tudo, lembre-se de que os diferentes modelos de avaliação (consulte pedido versus pedido normal) podem gerar "resultados" diferentes para a mesma expressão RT.
No código acima,
first
esecond
são referencialmente transparentes e, no entanto, a expressão no final gera "resultados" diferentes se avaliados na ordem normal e na ordem do aplicativo (no último, a expressão não é interrompida)..... o que leva ao uso de "resultado" entre aspas. Como não é necessário que uma expressão seja interrompida, ela pode não produzir um valor. Então, usar "resultado" é meio embaçado. Pode-se dizer que uma expressão RT sempre produz o mesmo
computations
em um modelo de avaliação.Terceiro, pode ser necessário ver duas
foo(50)
aparecendo no programa em locais diferentes como expressões diferentes - cada uma produzindo seus próprios resultados que podem diferir entre si. Por exemplo, se a linguagem permitir escopo dinâmico, ambas as expressões, embora sejam lexicamente idênticas, serão diferentes. Em perl:O escopo dinâmico confunde, porque facilita pensar que
x
é a única entrada parafoo
, quando, na realidade, éx
ey
. Uma maneira de ver a diferença é transformar o programa em um equivalente sem escopo dinâmico - ou seja, passando explicitamente os parâmetros, em vez de definirfoo(x)
, definimosfoo(x, y)
e passamosy
explicitamente nos chamadores.O ponto é que estamos sempre com uma
function
mentalidade: dada uma certa entrada para uma expressão, recebemos um "resultado" correspondente. Se dermos a mesma entrada, devemos sempre esperar o mesmo "resultado".Agora, e o código a seguir?
O
foo
procedimento interrompe o RT porque há redefinições. Ou seja, definimosy
em um ponto e, posteriormente, redefinimos o mesmoy
. No exemplo perl acima, osy
s são ligações diferentes, embora compartilhem o mesmo nome da letra "y". Aqui osy
são realmente os mesmos. É por isso que dizemos que (re) atribuição é uma meta operação: na verdade, você está alterando a definição do seu programa.Grosso modo, as pessoas geralmente descrevem a diferença da seguinte maneira: em um ambiente sem efeitos colaterais, você tem um mapeamento
input -> output
. Em um cenário "imperativo", você teminput -> ouput
no contexto de umstate
que pode mudar com o tempo.Agora, em vez de apenas substituir expressões por seus valores correspondentes, também é necessário aplicar transformações ao
state
em cada operação que requer (e, é claro, as expressões podem consultar o mesmostate
para realizar cálculos).Portanto, se em um programa livre de efeitos colaterais tudo o que precisamos saber para calcular uma expressão é sua entrada individual, em um programa imperativo, precisamos conhecer as entradas e todo o estado, para cada etapa computacional. O raciocínio é o primeiro a sofrer um grande golpe (agora, para depurar um procedimento problemático, você precisa da entrada e do core dump). Certos truques são impraticáveis, como memorização. Mas também simultaneidade e paralelismo se tornam muito mais desafiadores.
fonte