Como a transparência referencial é imposta?

8

Nas linguagens FP, chamar uma função com os mesmos parâmetros repetidamente retorna o mesmo resultado repetidamente (ou seja, transparência referencial).

Mas uma função como esta (pseudo-código):

function f(a, b) {
    return a + b + currentDateTime.seconds;
}

não retornará o mesmo resultado para os mesmos parâmetros.

Como esses casos são tratados no FP?

Como a transparência referencial é imposta? Ou não é e depende dos programadores se comportarem?

JohnDoDo
fonte
5
Depende do idioma, alguns não imporão transparência referencial, outros usam o sistema de tipos para separar funções referencialmente transparentes de E / S, por exemplo, mônadas em Haskell ou tipos de exclusividade em Clean
jk.
1
Um sistema de tipo bom impedirá que você faça chamadas currentDateTimede f, em idiomas que reforçam a transparência referencial (como Haskell). Vou deixar que outra pessoa fornecer uma resposta mais detalhada :) (dica: currentDateTimefaz IO, e isso vai mostrar em seu tipo)
Andres F.

Respostas:

20

ae bsão Numbers, enquanto currentDateTime.secondsretorna um IO<Number>. Esses tipos são incompatíveis, você não pode adicioná-los, portanto, sua função não é bem digitada e simplesmente não será compilada. Pelo menos é assim que é feito em linguagens puras com um sistema de tipo estático, como Haskell. Em linguagens impuras como ML, Scala ou F #, cabe ao programador garantir a transparência referencial e, é claro, em linguagens dinamicamente tipadas como Clojure ou Scheme, não existe um sistema de tipo estático para impor a transparência referencial.

Jörg W Mittag
fonte
Portanto, não é possível que o sistema de compilador / tipo garanta transparência referencial para mim no Scala, como no haskell?
Cib
8

Tentarei ilustrar a abordagem de Haskell (não tenho certeza de que minha intuição esteja 100% correta, pois não sou especialista em Haskell, as correções são bem-vindas).

Seu código pode ser escrito em Haskell da seguinte maneira:

import System.CPUTime

f :: Integer -> Integer -> IO Integer
f a b = do
          t <- getCPUTime
          return (a + b + (div t 1000000000000))

Então, onde está a transparência referencial? fé uma função que, dados dois inteiros ae b, criará uma ação, como você pode ver pelo tipo de retorno IO Integer. Essa ação sempre será a mesma, considerando os dois números inteiros; portanto, a função que mapeia um par de números inteiros para ações de IO é referencialmente transparente.

Quando essa ação é executada, o valor inteiro produzido depende do tempo atual da CPU: executar ações NÃO é uma aplicação de função.

Resumindo: No Haskell, você pode usar funções puras para construir e combinar ações complexas (seqüenciamento, composição de ações etc.) de maneira referencialmente transparente. Novamente, observe que no exemplo acima a função pura fnão retorna um número inteiro: ela retorna uma ação.

EDITAR

Mais alguns detalhes sobre a pergunta JohnDoDo.

O que significa que "executar ações NÃO é uma aplicação funcional"?

Dados os conjuntos T1, T2, Tn, T, uma função f é um mapeamento (relação) que se associa a cada tupla em T1 x T2 x ... x Tn um valor em T. Portanto, a aplicação da função produz um valor de saída, considerando alguns valores de entrada . Usando esse mecanismo, você pode construir expressões que avaliam valores, por exemplo, o valor 10é o resultado da avaliação da expressão 4 + 6. Observe que, ao mapear valores para valores dessa maneira, você não está executando nenhum tipo de entrada / saída.

Em Haskell, ações são valores de tipos especiais que podem ser construídos avaliando expressões contendo funções puras apropriadas que funcionam com ações. Dessa maneira, um programa Haskell é uma ação composta obtida pela avaliação da mainfunção. Esta ação principal tem tipo IO ().

Depois que essa ação composta é definida, outro mecanismo (não o aplicativo de funções) é usado para invocar / executar a ação (veja, por exemplo, aqui ). Toda a execução do programa é o resultado da invocação da ação principal, que por sua vez pode invocar sub-ações. Esse mecanismo de chamada (cujos detalhes internos eu não conheço) cuida da execução de todas as chamadas de E / S necessárias, possivelmente acessando o terminal, o disco, a rede e assim por diante.

Voltando ao exemplo. A função facima não retorna um número inteiro e você não pode escrever uma função que execute E / S e retorne um número inteiro ao mesmo tempo: você deve escolher um dos dois.

O que você pode fazer é incorporar a ação retornada por f 2 3em uma ação mais complexa. Por exemplo, se você deseja imprimir o número inteiro produzido por essa ação, você pode escrever:

main :: IO ()
main = do
          x <- f 2 3
          putStrLn (show x)

A donotação indica que a ação retornada pela função principal é obtida por uma composição seqüencial de duas ações menores e a x <-notação indica que o valor produzido na primeira ação deve ser passado para a segunda ação.

Na segunda ação

putStrLn (show x)

o nome xé vinculado ao número inteiro produzido executando a ação

f 2 3

Um ponto importante é que o número inteiro produzido quando a primeira ação é chamada pode viver apenas dentro das ações de E / S: pode ser passado de uma ação de E / S para a seguinte, mas não pode ser extraído como um valor inteiro simples.

Compare a mainfunção acima com esta:

main = do
      let y = 2 + 3
      putStrLn (show y)

Nesse caso, existe apenas uma ação, a saber putStrLn (show y), e yestá vinculada ao resultado da aplicação da função pura +. Também podemos definir esta ação principal da seguinte maneira:

main = putStrLn "5"

Então, observe a sintaxe diferente

x <- f 2 3    -- Inject the value produced by an action into
              -- the following IO actions.
              -- The value may depend on when the action is
              -- actually executed. What happens when the action is
              -- executed is not known here: it may get user input,
              -- access the disk, the network, the system clock, etc.

let y = 2 + 3 -- Bind y to the result of applying the pure function `+`
              -- to the arguments 2 and 3.
              -- The value depends only on the arguments 2 and 3.

Sumário

  • No Haskell, funções puras são usadas para construir as ações que constituem um programa.
  • Ações são valores de um tipo especial.
  • Como as ações são construídas aplicando funções puras, a construção de ações é referencialmente transparente.
  • Após a construção de uma ação, ela pode ser chamada usando um mecanismo separado.
Giorgio
fonte
2
Você se importaria em detalhar um pouco a executing actions is NOT function applicationfrase? No meu exemplo, pretendia retornar um número inteiro. O que acontece se um retorno é um número inteiro?
JohnDoDo
2
O @JohnDoDo em Haskell pelo menos com preguiça (não consigo falar com nenhuma linguagem referencialmente transparente), nada é executado até que absolutamente deva ser. Isto significa que no exemplo Giorgio mostrou que você está recebendo essa ação, e fora de fazer as coisas desagradáveis que você nunca pode obter o número para fora de uma ação IO, em vez você deve combinar essa ação com outras ações IO todo o seu programa até que você acabar com Main qual; surpresa surpresa é uma ação de IO. O próprio Haskell executa a ação de E / S, mas durante a execução apenas as partes necessárias e somente quando são.
Jimmy Hoffa
@JohnDoDo Se você quiser retornar um número inteiro, fnão poderá ter o tipo IO Integer(isso é uma ação, não um número inteiro). Mas, então, ele não pode chamar a "data atual", que tem tipo IO Integer.
Andrés F.
Além disso, o número inteiro de E / S que você obtém como saída não pode ser convertido novamente em um número inteiro normal e, em seguida, ser usado novamente em código puro. Essencialmente, o que acontece na mônada IO permanece na mônada IO. (Há uma exceção a isso, você pode usar unsafePerformIO para recuperar um valor, mas, ao fazer isso, você basicamente diz ao compilador que "Tudo bem, isso é realmente referencialmente transparente". O compilador acreditará em você e no próxima vez que você usar a função, ele pode buscar o valor da função de TI calculado antes, em vez de usar o tempo atual).
Michael Shaw
1
Todo o seu último exemplo mostra é que você não tem uma instância correspondente Showdisponível. Mas você pode facilmente adicionar um, caso em que o código será compilado e executado muito bem. Não há nada de especial em IOações com relação a show.
4

A abordagem usual é permitir que o compilador rastreie se uma função é pura por todo o gráfico de chamadas e rejeite o código que declara funções como puras que fazem coisas impuras (onde "chamar uma função impura" também é uma coisa impura).

Haskell faz isso tornando tudo puro na própria linguagem; qualquer coisa impura é executada no tempo de execução, não a própria linguagem. A linguagem apenas constrói ações de E / S usando funções puras. O tempo de execução encontra a função pura chamada maindo Mainmódulo designado , avalia-a e executa a ação (impura) resultante.

Outras línguas são mais pragmáticas sobre isso; Uma abordagem comum é adicionar sintaxe para marcar as funções como 'puras' e proibir qualquer ação impura (atualizações de variáveis, chamar funções impuras, construções de E / S) dentro dessas funções.

No seu exemplo, currentDateTimeé uma função impura (ou algo que se comporta como um), portanto, é proibido chamá-la dentro de um bloco puro e causar um erro no compilador. No Haskell, sua função seria algo como isto:

f :: Int -> Int -> IO Int
f a b = do
    ct <- getCurrentTime
    return (a + b + timeSeconds ct)

Se você tentou fazer isso em uma função não IO, assim:

f :: Int -> Int -> Int
f a b =
    let ct = getCurrentTime
    in a + b + timeSeconds ct

... então o compilador diria que seus tipos não fazem check-out - getCurrentTimeé do tipo IO Time, não Time, mas timeSecondsespera Time. Em outras palavras, Haskell utiliza seu sistema de tipos para modelar (e reforçar) a pureza.

tdammers
fonte