O que é transparência referencial?


38

Eu vi isso em paradigmas imperativos

f (x) + f (x)

pode não ser o mesmo que:

2 * f (x)

Mas, em um paradigma funcional, deve ser o mesmo. Eu tentei implementar os dois casos em Python e Scheme , mas para mim eles parecem bem simples.

Qual seria um exemplo que poderia apontar a diferença com a função fornecida?


7
Você pode, e costuma fazer, escrever funções referencialmente transparentes em python. A diferença é que o idioma não o aplica.
Karl Bielefeldt

5
em C e afins: f(x++)+f(x++)pode não ser o mesmo que 2*f(x++)(? em C é especialmente agradável quando coisas como o que está escondido dentro de macros - fez I quebrou meu nariz em que você aposta)
mosquito

No meu entendimento, o exemplo do @ gnat é por que linguagens orientadas para a funcionalidade, como R, empregam passagem por referência e explicitamente evitam funções que modificam seus argumentos. No R, pelo menos, pode ser difícil contornar essas restrições (pelo menos de maneira estável e portátil) sem se aprofundar no complicado sistema de ambientes e espaços de nomes e caminhos de pesquisa da linguagem.
shadowtalker 25/08

4
@ssdecontrol: Na verdade, quando você tem transparência referencial, passagem por valor e passagem por referência sempre produzem exatamente o mesmo resultado, portanto, não importa qual o idioma usado. Linguagens funcionais são freqüentemente especificadas com algo semelhante ao valor de passagem para maior clareza semântica, mas suas implementações geralmente usam referência de desempenho para desempenho (ou até ambas, dependendo de qual é mais rápido para o contexto fornecido).
Jörg W Mittag

4
@gnat: Em particular, f(x++)+f(x++)pode ser absolutamente qualquer coisa, já que está invocando um comportamento indefinido. Mas isso não está realmente relacionado à transparência referencial - o que não ajudaria nesta chamada, é 'indefinido' para funções referencialmente transparentes como sin(x++)+sin(x++)também. Poderia ser 42, pode formatar seu disco rígido, poderia ter demônios voando para fora do nariz usuários ...
Christopher Creutzig

Respostas:


62

A transparência referencial, referida a uma função, indica que você pode determinar o resultado da aplicação dessa função apenas observando os valores de seus argumentos. Você pode escrever funções transparentemente referenciais em qualquer linguagem de programação, por exemplo, Python, Scheme, Pascal, C.

Por outro lado, na maioria dos idiomas, você também pode escrever funções transparentes e não referenciais. Por exemplo, esta função Python:

counter = 0

def foo(x):
  global counter

  counter += 1
  return x + counter

não é referencialmente transparente, chamando de fato

foo(x) + foo(x)

e

2 * foo(x)

produzirá valores diferentes, para qualquer argumento x. A razão para isso é que a função usa e modifica uma variável global; portanto, o resultado de cada chamada depende desse estado de alteração e não apenas do argumento da função.

Haskell, uma linguagem puramente funcional, separa estritamente a avaliação da expressão na qual funções puras são aplicadas e sempre referencialmente transparentes, da execução da ação (processamento de valores especiais), que não é referencialmente transparente, ou seja, executar a mesma ação sempre que um resultado diferente.

Portanto, para qualquer função Haskell

f :: Int -> Int

e qualquer número inteiro x, é sempre verdade que

2 * (f x) == (f x) + (f x)

Um exemplo de uma ação é o resultado da função de biblioteca getLine:

getLine :: IO String

Como resultado da avaliação da expressão, essa função (na verdade uma constante) produz primeiro um valor puro do tipo IO String. Valores desse tipo são valores como qualquer outro: você pode distribuí-los, colocá-los em estruturas de dados, compor usando funções especiais e assim por diante. Por exemplo, você pode fazer uma lista de ações da seguinte forma:

[getLine, getLine] :: [IO String]

As ações são especiais, pois você pode dizer ao tempo de execução do Haskell para executá-las escrevendo:

main = <some action>

Nesse caso, quando o programa Haskell é iniciado, o tempo de execução percorre a ação vinculada maine a executa , possivelmente produzindo efeitos colaterais. Portanto, a execução da ação não é referencialmente transparente porque a execução da mesma ação duas vezes pode produzir resultados diferentes, dependendo do que o tempo de execução obtém como entrada.

Graças ao sistema de tipos de Haskell, uma ação nunca pode ser usada em um contexto em que outro tipo é esperado e vice-versa. Portanto, se você deseja encontrar o comprimento de uma string, pode usar a lengthfunção:

length "Hello"

retornará 5. Mas se você quiser encontrar o comprimento de uma string lida no terminal, não poderá escrever

length (getLine)

porque você recebe um erro de tipo: lengthespera uma entrada do tipo lista (e uma String é, de fato, uma lista), mas getLineé um valor do tipo IO String(uma ação). Dessa maneira, o sistema de tipos garante que um valor de ação como getLine(cuja execução é executada fora da linguagem principal e que pode ser transparente não referencialmente) não possa ser oculto dentro de um valor de tipo não-ação Int.

EDITAR

Para responder à pergunta anterior, aqui está um pequeno programa Haskell que lê uma linha do console e imprime seu comprimento.

main :: IO () -- The main program is an action of type IO ()
main = do
          line <- getLine
          putStrLn (show (length line))

A ação principal consiste em duas subações que são executadas sequencialmente:

  1. getlinede tipo IO String,
  2. o segundo é construído avaliando a função putStrLndo tipo String -> IO ()em seu argumento.

Mais precisamente, a segunda ação é construída por

  1. vinculação lineao valor lido pela primeira ação,
  2. avaliar as funções puras length(calcular o comprimento como um número inteiro) e depois show(transformar o número inteiro em uma sequência de caracteres),
  3. construindo a ação aplicando a função putStrLnao resultado de show.

Nesse ponto, a segunda ação pode ser executada. Se você digitou "Olá", ele imprimirá "5".

Observe que, se você obtiver um valor de uma ação usando a <-notação, você só poderá usar esse valor dentro de outra ação, por exemplo, você não poderá escrever:

main = do
          line <- getLine
          show (length line) -- Error:
                             -- Expected type: IO ()
                             --   Actual type: String

porque show (length line)tem type Stringenquanto a notação requer que uma ação ( getLinedo tipo IO String) seja seguida por outra ação (por exemplo, putStrLn (show (length line))do tipo IO ()).

EDIT 2

A definição de transparência referencial de Jörg W. Mittag é mais geral que a minha (eu votei positivamente sua resposta). Eu usei uma definição restrita porque o exemplo da pergunta se concentra no valor de retorno das funções e queria ilustrar esse aspecto. No entanto, RT geralmente se refere ao significado de todo o programa, incluindo alterações no estado global e interações com o ambiente (IO) causadas pela avaliação de uma expressão. Portanto, para uma definição geral correta, você deve consultar essa resposta.


10
O downvoter pode sugerir como posso melhorar esta resposta?
Giorgio

Então, como obter o comprimento de uma string lida no terminal em Haskell?
precisa saber é o seguinte

2
Isso é extremamente pedante, mas por uma questão de integridade, não é o sistema de tipos de Haskell que garante que ações e funções puras não se misturem; é o fato de o idioma não fornecer nenhuma função impura que você possa chamar diretamente. Você pode realmente implementar o IOtipo de Haskell facilmente em qualquer idioma com lambdas e genéricos, mas como qualquer pessoa pode ligar printlndiretamente, a implementação IOnão garante pureza; seria apenas uma convenção.
Dock

Eu quis dizer que (1) todas as funções são puras (é claro, são puras porque a linguagem não fornece nenhuma impura), mesmo que eu saiba que existem alguns mecanismos para contornar isso), e (2) funções puras e ações impuras têm tipos diferentes, portanto não podem ser misturadas. BTW, o que você quer dizer com chamada diretamente ?
Giorgio

6
Seu argumento sobre getLinenão ser referencialmente transparente está incorreto. Você está apresentando getLinecomo se ele avalia ou se reduz a alguma String, cuja String específica depende da entrada do usuário. Isto está incorreto. IO Stringnão contém uma String mais do que contém Maybe String. IO Stringé uma receita para talvez, possivelmente obter uma String e, como expressão, tão pura quanto qualquer outra em Haskell.
LuxuryMode

25
def f(x): return x()

from random import random
f(random) + f(random) == 2*f(random)
# => False

No entanto, não é isso que significa transparência referencial. RT significa que você pode substituir qualquer expressão no programa pelo resultado da avaliação dessa expressão (ou vice-versa) sem alterar o significado do programa.

Tome, por exemplo, o seguinte programa:

def f(): return 2

print(f() + f())
print(2)

Este programa é referencialmente transparente. Posso substituir uma ou ambas as ocorrências de f()com 2e ainda funcionará da mesma maneira:

def f(): return 2

print(2 + f())
print(2)

ou

def f(): return 2

print(f() + 2)
print(2)

ou

def f(): return 2

print(2 + 2)
print(f())

todos irão se comportar da mesma maneira.

Bem, na verdade, eu traí. Eu deveria ser capaz de substituir a chamada printpor seu valor de retorno (que não é de todo valor) sem alterar o significado do programa. No entanto, claramente, se eu remover as duas printinstruções, o significado do programa mudará: antes, ele imprimia algo na tela, depois não o fazia. A E / S não é referencialmente transparente.

A regra simples é: se você pode substituir qualquer chamada de expressão, sub-expressão ou sub-rotina pelo valor de retorno dessa chamada de expressão, sub-expressão ou sub-rotina em qualquer lugar do programa, sem que o programa mude seu significado, você tem referências transparência. E o que isso significa, na prática, é que você não pode ter nenhuma E / S, não pode ter nenhum estado mutável, não pode ter efeitos colaterais. Em toda expressão, o valor da expressão deve depender apenas dos valores das partes constituintes da expressão. E em toda chamada de sub-rotina, o valor de retorno deve depender apenas dos argumentos.


4
"não pode ter nenhum estado mutável": Bem, você pode tê-lo se estiver oculto e não influenciar o comportamento observável do seu código. Pense, por exemplo, na memorização.
Giorgio

4
@ Giorgio: Isso é talvez subjetivo, mas eu argumentaria que os resultados armazenados em cache não são realmente "estado mutável" se estiverem ocultos e não tiverem efeitos observáveis. A imutabilidade é sempre uma abstração implementada em cima de hardware mutável; freqüentemente é fornecido pelo idioma (fornecendo a abstração de "um valor", mesmo que o valor possa se mover entre registradores e locais de memória durante a execução e pode desaparecer quando se sabe que nunca será usado novamente), mas não é menos válido quando é fornecida por uma biblioteca ou outros enfeites. (Assumindo que é implementado corretamente, é claro.)
ruach

1
+1 Eu realmente gosto do printexemplo. Talvez uma maneira de ver isso é que o que é impresso na tela faz parte do "valor de retorno". Se você pode substituir printcom seu valor de retorno de função e a gravação equivalente no terminal, o exemplo funciona.
Pierre Arlaud

1
@Giorgio O uso do espaço / tempo não pode ser considerado um efeito colateral para fins de transparência referencial. Isso tornaria 4e 2 + 2não intercambiável, pois eles têm diferentes tempos de execução, e o ponto principal da transparência referencial é que você pode substituir uma expressão pelo que ela avaliar. A consideração importante seria a segurança do thread.
Doval

1
@overexchange: transparência referencial significa que você pode substituir cada subexpressão pelo seu valor sem alterar o significado do programa. listOfSequence.append(n)retorna None, portanto, você poderá substituir todas as chamadas listOfSequence.append(n)por Nonesem alterar o significado do seu programa. Você pode fazer aquilo? Caso contrário, não é referencialmente transparente.
Jörg W Mittag 09/02

1

Partes desta resposta são obtidas diretamente de um tutorial inacabado de programação funcional , hospedado na minha conta do GitHub:

Diz-se que uma função é referencialmente transparente se, dada os mesmos parâmetros de entrada, sempre produz a mesma saída (valor de retorno). Se alguém procura uma raison d'être para pura programação funcional, a transparência referencial é um bom candidato. Ao raciocinar com fórmulas em álgebra, aritmética e lógica, essa propriedade - também chamada substitutividade de iguais por iguais - é tão fundamentalmente importante que geralmente é um dado adquirido ...

Considere um exemplo simples:

x = 42

Em uma linguagem funcional pura, o lado esquerdo e o lado direito do sinal de igual são substituíveis um pelo outro nos dois sentidos. Ou seja, diferente de uma linguagem como C, a notação acima realmente afirma uma igualdade. Uma conseqüência disso é que podemos raciocinar sobre o código do programa como equações matemáticas.

Do wiki Haskell :

Cálculos puros produzem o mesmo valor cada vez que são chamados. Essa propriedade é chamada transparência referencial e possibilita a condução de raciocínio equacional no código ...

Para contrastar, o tipo de operação executada por linguagens do tipo C às vezes é chamado de atribuição destrutiva .

O termo puro é frequentemente usado para descrever uma propriedade de expressões, relevante para esta discussão. Para que uma função seja considerada pura,

  • não é permitido exibir efeitos colaterais e
  • deve ser referencialmente transparente.

De acordo com a metáfora da caixa preta, encontrada em numerosos livros matemáticos, os internos de uma função são completamente isolados do mundo exterior. Um efeito colateral é quando uma função ou expressão viola esse princípio - ou seja, é permitido que o procedimento se comunique de alguma forma com outras unidades de programa (por exemplo, para compartilhar e trocar informações).

Em resumo, a transparência referencial é uma obrigação para que as funções se comportem como verdadeiras , funções matemáticas também na semântica das linguagens de programação.


isso parece abrir com uma cópia palavra por palavra tirada daqui : "Diz-se que uma função é referencialmente transparente se, dados os mesmos parâmetros de entrada, sempre produz a mesma saída ..." O Stack Exchange tem regras para plágio , você está ciente disso? "O plágio é o ato sem alma de copiar pedaços do trabalho de outra pessoa, dar um tapa no seu nome e passar por você como o autor original ..."
gnat

3
Eu escrevi essa página.
yesthisisuser

se for esse o caso, considere fazer com que pareça menos plágio - porque os leitores não têm como dizer. Você sabe como fazer isso no SE? 1) Você refere a fonte dos originais, como "Como (escrevi) [here](link to source)...", seguido de 2) formatação de citação adequada (use aspas, ou, melhor ainda, > símbolo para isso). Também não faria mal se além de dar uma orientação geral, os endereços de responder à pergunta concreta perguntado sobre, neste caso cerca de f(x)+f(x)/ 2*f(x), consulte Como responder - caso contrário, pode parecer que você está simplesmente anunciando sua página
mosquito

1
Teoricamente, eu entendi essa resposta. Mas, praticamente seguindo essas regras, preciso retornar a lista de seqüências de granizo neste programa . Como eu faço isso?
Overexchange
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.