A maioria das fontes define uma função pura como tendo as duas propriedades a seguir:
- Seu valor de retorno é o mesmo para os mesmos argumentos.
- Sua avaliação não tem efeitos colaterais.
É a primeira condição que me preocupa. Na maioria dos casos, é fácil julgar. Considere as seguintes funções JavaScript (conforme mostrado neste artigo )
Puro:
const add = (x, y) => x + y;
add(2, 4); // 6
Impuro:
let x = 2;
const add = (y) => {
return x += y;
};
add(4); // x === 6 (the first time)
add(4); // x === 10 (the second time)
É fácil ver que a 2ª função fornecerá saídas diferentes para chamadas subseqüentes, violando a primeira condição. E, portanto, é impuro.
Esta parte eu recebo.
Agora, para minha pergunta, considere esta função que converte uma determinada quantia em dólares em euros:
(EDIT - Usando const
na primeira linha. Utilizado let
anteriormente inadvertidamente.)
const exchangeRate = fetchFromDatabase(); // evaluates to say 0.9 for today;
const dollarToEuro = (x) => {
return x * exchangeRate;
};
dollarToEuro(100) //90 today
dollarToEuro(100) //something else tomorrow
Suponha que buscamos a taxa de câmbio de um banco de dados e ela muda todos os dias.
Agora, não importa quantas vezes eu chame essa função hoje , ela fornecerá a mesma saída para a entrada 100
. No entanto, isso pode me dar uma saída diferente amanhã. Não tenho certeza se isso viola a primeira condição ou não.
IOW, a função em si não contém nenhuma lógica para alterar a entrada, mas depende de uma constante externa que pode mudar no futuro. Nesse caso, é absolutamente certo que isso mudará diariamente. Em outros casos, isso pode acontecer; talvez não.
Podemos chamar essas funções de funções puras. Se a resposta for NÃO, como então podemos refatorá-la para ser uma?
fonte
function myNumber(n) { this.n = n; }; myNumber.prototype.valueOf = function() { console.log('impure'); return this.n; }; const n = new myNumber(42); add(n, 1);
(x) => {return x * 0.9;}
. Amanhã, você terá uma função diferente, que também será pura, talvez(x) => {return x * 0.89;}
. Observe que cada vez que você executa,(x) => {return x * exchangeRate;}
ela cria uma nova função, e essa função é pura porqueexchangeRate
não pode mudar.const dollarToEuro = (x, exchangeRate) => { return x * exchangeRate; };
para uma função pura,Its return value is the same for the same arguments.
deve manter sempre, um segundo, 1 década .. mais tarde não importa o queRespostas:
O
dollarToEuro
valor de retorno do depende de uma variável externa que não é um argumento; portanto, a função é impura.Uma opção é passar
exchangeRate
. Dessa forma, toda vez que os argumentos são(something, somethingElse)
, é garantido que a saída sejasomething * somethingElse
:Observe que, para a programação funcional, você deve evitar
let
- sempre useconst
para evitar a reatribuição.fonte
const add = x => y => x + y; const one = add(42);
aqui ambasadd
eone
são funções puras.const foo = 42; const add42 = x => x + foo;
<- essa é outra função pura, que novamente usa variáveis livres.dollarToEuro
função no exemplo da sua resposta é impura porque depende da variável livreexchangeRate
. Isso é um absurdo. Como zerkms apontou, a pureza de uma função não tem nada a ver com o fato de possuir ou não variáveis livres. No entanto, zerkms também está errado porque ele acredita que adollarToEuro
função é impura porque depende daexchangeRate
origem de um banco de dados. Ele diz que é impuro porque "depende do pedido de informação transitivamente".dollarToEuro
é impuro porqueexchangeRate
é uma variável livre. Sugere que seexchangeRate
não fosse uma variável livre, ou seja, se fosse um argumento,dollarToEuro
seria puro. Por isso, sugere quedollarToEuro(100)
é impuro, masdollarToEuro(100, exchangeRate)
é puro. Isso é claramente absurdo, porque nos dois casos você depende doexchangeRate
que vem de um banco de dados. A única diferença é se é ou nãoexchangeRate
uma variável livre dentro dadollarToEuro
função.Tecnicamente, qualquer programa que você executa em um computador é impuro porque, eventualmente, compila instruções como "mover esse valor para
eax
" e "adicionar esse valor ao conteúdo deeax
", que são impuros. Isso não é muito útil.Em vez disso, pensamos em pureza usando caixas pretas . Se algum código sempre produz as mesmas saídas quando recebe as mesmas entradas, é considerado puro. Por essa definição, a função a seguir também é pura, embora internamente use uma tabela de notas impuras.
Não nos importamos com os internos porque estamos usando uma metodologia de caixa preta para verificar a pureza. Da mesma forma, não nos importamos que todo o código seja eventualmente convertido em instruções impuras da máquina, porque estamos pensando em pureza usando uma metodologia de caixa preta. Internos não são importantes.
Agora, considere a seguinte função.
A
greet
função é pura ou impura? Pela nossa metodologia de caixa preta, se fornecermos a mesma entrada (por exemplo,World
), ela sempre imprimirá a mesma saída na tela (por exemploHello World!
). Nesse sentido, não é puro? Não, não é. A razão pela qual não é pura é porque consideramos a impressão de algo na tela um efeito colateral. Se nossa caixa preta produz efeitos colaterais, ela não é pura.O que é um efeito colateral? É aqui que o conceito de transparência referencial é útil. Se uma função é referencialmente transparente, sempre podemos substituir aplicativos dessa função pelos seus resultados. Observe que isso não é o mesmo que função embutida .
Na função inlining, substituímos aplicativos de uma função pelo corpo da função sem alterar a semântica do programa. No entanto, uma função referencialmente transparente pode sempre ser substituída pelo seu valor de retorno sem alterar a semântica do programa. Considere o seguinte exemplo.
Aqui, destacamos a definição de
greet
e não mudou a semântica do programa.Agora, considere o seguinte programa.
Aqui, substituímos as aplicações do
greet
função pelos seus valores de retorno e isso mudou a semântica do programa. Não estamos mais imprimindo saudações na tela. Essa é a razão pela qual a impressão é considerada um efeito colateral, e é por isso que agreet
função é impura. Não é referencialmente transparente.Agora, vamos considerar outro exemplo. Considere o seguinte programa.
Claramente, a
main
função é impura. No entanto, é otimeDiff
função é pura ou impura? Embora dependa deserverTime
origem de uma chamada de rede impura, ela ainda é referencialmente transparente porque retorna as mesmas saídas para as mesmas entradas e porque não tem efeitos colaterais.Os zerkms provavelmente discordarão de mim neste ponto. Em sua resposta , ele disse que a
dollarToEuro
função no exemplo a seguir é impura porque "depende do IO transitivamente".Eu tenho que discordar dele porque o fato de o
exchangeRate
veio de um banco de dados é irrelevante. É um detalhe interno e nossa metodologia de caixa preta para determinar a pureza de uma função não se importa com detalhes internos.Em linguagens puramente funcionais como Haskell, temos uma saída para executar efeitos arbitrários de E / S. É chamado
unsafePerformIO
e, como o nome indica, se você não o usar corretamente, não será seguro, pois pode quebrar a transparência referencial. No entanto, se você sabe o que está fazendo, é perfeitamente seguro usá-lo.Geralmente é usado para carregar dados de arquivos de configuração perto do início do programa. Carregar dados de arquivos de configuração é uma operação de E / S impura. No entanto, não queremos ser sobrecarregados ao passar os dados como entradas para todas as funções. Portanto, se usarmos
unsafePerformIO
, podemos carregar os dados no nível superior e todas as nossas funções puras podem depender dos imutáveis dados globais de configuração.Observe que apenas porque uma função depende de alguns dados carregados de um arquivo de configuração, um banco de dados ou uma chamada de rede, não significa que a função seja impura.
No entanto, vamos considerar o seu exemplo original, que tem semântica diferente.
Aqui, estou assumindo que, porque
exchangeRate
não está definido comoconst
, será modificado enquanto o programa estiver em execução. Se for esse o caso, entãodollarToEuro
é definitivamente uma função impura, porque quando oexchangeRate
é modificada, ela quebra a transparência referencial.No entanto, se a
exchangeRate
variável não for modificada e nunca será modificada no futuro (ou seja, se for um valor constante), mesmo que seja definida comolet
, ela não quebrará a transparência referencial. Nesse caso,dollarToEuro
é de fato uma função pura.Observe que o valor de
exchangeRate
pode mudar sempre que você executar o programa novamente e não quebrará a transparência referencial. Ele só quebra a transparência referencial se for alterado enquanto o programa estiver em execução.Por exemplo, se você executar meu
timeDiff
exemplo várias vezes, obterá valores diferentes paraserverTime
e, portanto, resultados diferentes. No entanto, como o valor deserverTime
nunca muda enquanto o programa está sendo executado, atimeDiff
função é pura.fonte
const
no meu exemplo.const
, adollarToEuro
função é realmente pura. A única maneira deexchangeRate
mudar o valor é se você executasse o programa novamente. Nesse caso, o processo antigo e o novo processo são diferentes. Portanto, não quebra a transparência referencial. É como chamar uma função duas vezes com argumentos diferentes. Os argumentos podem ser diferentes, mas dentro da função o valor dos argumentos permanece constante.eax
for limpo - por meio de uma carga ou de uma limpeza - o código permanece determinístico, independentemente de o que mais está acontecendo e é, portanto, pura Caso contrário, a resposta muito abrangente..A resposta de um eu-purista (onde "eu" sou literalmente eu, pois acho que essa pergunta não tem um único formal). resposta "correta"):
Em uma linguagem dinâmica como o JS, com tantas possibilidades de descascar tipos de base de patches ou criar tipos personalizados usando recursos como
Object.prototype.valueOf
é impossível dizer se uma função é pura apenas olhando para ela, uma vez que cabe ao interlocutor se eles desejam para produzir efeitos colaterais.Uma demonstração:
Uma resposta do meu pragmático:
De própria definição da wikipedia
Em outras palavras, importa apenas como uma função se comporta, não como é implementada. E desde que uma função específica mantenha essas duas propriedades - é pura, independentemente de como exatamente ela foi implementada.
Agora, para sua função:
É impuro porque não qualifica o requisito 2: depende do IO transitivamente.Concordo que a afirmação acima está errada, consulte a outra resposta para obter detalhes: https://stackoverflow.com/a/58749249/251311
Outros recursos relevantes:
fonte
me
como zerkms que fornece uma resposta.add42
e minhaaddX
é puramente que minhax
pode ser alterada e suaft
não pode ser alterada (e, portanto,add42
o valor de retorno da variável não varia com baseft
)?dollarToEuro
função no seu exemplo seja impura. Expliquei por que discordo da minha resposta. stackoverflow.com/a/58749249/783743Como outras respostas disseram, da maneira que você implementou
dollarToEuro
,é realmente puro, porque a taxa de câmbio não é atualizada enquanto o programa está sendo executado. Conceitualmente, no entanto,
dollarToEuro
parece que deve ser uma função impura, na medida em que usa a taxa de câmbio mais atualizada. A maneira mais simples de explicar esta discrepância é que você não implementaramdollarToEuro
masdollarToEuroAtInstantOfProgramStart
.A chave aqui é que existem vários parâmetros necessários para calcular uma conversão de moeda e que uma versão verdadeiramente pura do general
dollarToEuro
forneceria todos eles. Os parâmetros mais diretos são a quantidade de USD a ser convertida e a taxa de câmbio. No entanto, como você deseja obter sua taxa de câmbio com base nas informações publicadas, agora você tem três parâmetros para fornecer:A autoridade histórica aqui é seu banco de dados e, assumindo que o banco de dados não está comprometido, sempre retornará o mesmo resultado para a taxa de câmbio em um dia específico. Portanto, com a combinação desses três parâmetros, você pode escrever uma versão totalmente pura e auto-suficiente do general
dollarToEuro
, que pode ser algo como isto:Sua implementação captura valores constantes para a autoridade histórica e a data da transação no instante em que a função é criada - a autoridade histórica é seu banco de dados e a data capturada é a data em que você inicia o programa - tudo o que resta é o valor em dólar , que o chamador fornece. A versão impura do
dollarToEuro
disso sempre obtém o valor mais atualizado, essencialmente pega o parâmetro date implicitamente, configurando-o no instante em que a função é chamada, o que não é puro simplesmente porque você nunca pode chamar a função com os mesmos parâmetros duas vezes.Se você deseja ter uma versão pura do
dollarToEuro
ainda possa obter o valor mais atualizado, ainda pode vincular a autoridade histórica, mas deixe o parâmetro date acoplado e solicite a data ao chamador como argumento, terminando com algo assim:fonte
Gostaria de recuar um pouco dos detalhes específicos de JS e da abstração de definições formais, e falar sobre quais condições precisam ser mantidas para permitir otimizações específicas. Essa é geralmente a principal coisa com a qual nos preocupamos ao escrever um código (embora também ajude a provar a correção). A programação funcional não é um guia para as últimas modas nem um voto monástico de abnegação. É uma ferramenta para resolver problemas.
Quando você tem um código como este:
Se
exchangeRate
nunca puder ser modificado entre as duas chamadas paradollarToEuro(100)
, é possível memorizar o resultado da primeira chamadadollarToEuro(100)
e otimizar a segunda chamada. O resultado será o mesmo, para que possamos lembrar o valor anterior.A
exchangeRate
pode ser definido uma vez, antes de chamar qualquer função que olha-lo, e nunca modificado. Menos restritivo, você pode ter um código que procuraexchangeRate
uma função ou bloco de código específico e usa a mesma taxa de câmbio de forma consistente nesse escopo. Ou, se apenas esse encadeamento puder modificar o banco de dados, você poderá assumir que, se não atualizou a taxa de câmbio, ninguém mais a alterou.Se
fetchFromDatabase()
ela própria é uma função pura avaliando uma constante, eexchangeRate
é imutável, poderíamos dobrar essa constante durante todo o cálculo. Um compilador que sabe que esse é o caso pode fazer a mesma dedução que você fez no comentário,dollarToEuro(100)
avaliada como 90.0 e substitui a expressão inteira pela constante 90.0.No entanto, se
fetchFromDatabase()
não executar E / S, o que é considerado um efeito colateral, seu nome viola o Princípio de menor espanto.fonte
Esta função não é pura, depende de uma variável externa, que quase definitivamente vai mudar.
Portanto, a função falha no primeiro ponto que você mencionou, mas não retorna o mesmo valor para os mesmos argumentos.
Para tornar essa função "pura", passe
exchangeRate
como argumento.Isso satisfaria as duas condições.
Código de exemplo:
fonte
const
.Para expandir os pontos que outros fizeram sobre transparência referencial: podemos definir pureza como sendo simplesmente transparência referencial de chamadas de função (ou seja, todas as chamadas para a função podem ser substituídas pelo valor de retorno sem alterar a semântica do programa).
As duas propriedades que você fornece são ambas consequências da transparência referencial. Por exemplo, a função a seguir
f1
é impura, pois não fornece o mesmo resultado sempre (a propriedade que você numerou 1):Por que é importante obter sempre o mesmo resultado? Como obter resultados diferentes é uma maneira de uma chamada de função ter semântica diferente de um valor e, portanto, quebrar a transparência referencial.
Digamos que escrevemos o código
f1("hello", "world")
, executamos e obtemos o valor de retorno"hello"
. Se fizermos uma busca / substituição de cada chamadaf1("hello", "world")
e substituí-la por"hello"
, teremos alterado a semântica do programa (todas as chamadas serão substituídas por agora"hello"
, mas originalmente cerca de metade delas teria avaliado"world"
). Portanto, as chamadas paraf1
não são referencialmente transparentes, portanto,f1
são impuras.Outra maneira pela qual uma chamada de função pode ter semântica diferente de um valor é executando instruções. Por exemplo:
O valor de retorno
f2("bar")
será sempre"bar"
, mas a semântica do valor"bar"
é diferente da chamada,f2("bar")
pois o último também registrará no console. Substituir um pelo outro alteraria a semântica do programa, portanto não é referencialmente transparente e, portanto,f2
impuro.Se a sua
dollarToEuro
função é referencialmente transparente (e, portanto, pura) depende de duas coisas:exchangeRate
mudança será alguma vez dentro desse 'escopo'Não há "melhor" escopo para usar; normalmente pensamos em uma única execução do programa ou na vida útil do projeto. Como analogia, imagine que os valores de retorno de todas as funções sejam armazenados em cache (como a tabela de memorando no exemplo dado por @ aadit-m-shah): quando precisaríamos limpar o cache, para garantir que valores obsoletos não interfiram em nosso semântica?
Se
exchangeRate
estivesse usandovar
, ele poderia mudar entre cada chamada paradollarToEuro
; precisaríamos limpar os resultados armazenados em cache entre cada chamada, para que não houvesse transparência referencial.Ao usar
const
, estamos expandindo o 'escopo' para uma execução do programa: seria seguro armazenar em cache os valores de retornodollarToEuro
até o término do programa. Poderíamos imaginar o uso de uma macro (em uma linguagem como Lisp) para substituir chamadas de função por seus valores de retorno. Essa quantidade de pureza é comum para itens como valores de configuração, opções de linha de comando ou IDs exclusivos. Se nos limitarmos a pensar em uma execução do programa, obteremos a maioria dos benefícios da pureza, mas temos que ter cuidado ao longo das execuções (por exemplo, salvando dados em um arquivo e carregando-o em outra execução). Eu não chamaria essas funções de "puras" em um resumo sentido (por exemplo, se eu estivesse escrevendo uma definição de dicionário), mas não tenho problema em tratá-las como puras no contexto .Se tratarmos a vida útil do projeto como nosso 'escopo', então somos os "mais referencialmente transparentes" e, portanto, os "mais puros", mesmo em um sentido abstrato. Nunca precisaríamos limpar nosso cache hipotético. Poderíamos até fazer esse "cache" reescrevendo diretamente o código-fonte no disco, para substituir as chamadas pelos seus valores de retorno. Isso funcionaria mesmo em projetos, por exemplo, poderíamos imaginar um banco de dados on-line de funções e seus valores de retorno, onde qualquer pessoa pode procurar uma chamada de função e (se estiver no DB) usar o valor de retorno fornecido por alguém do outro lado do mundo que usou uma função idêntica anos atrás em um projeto diferente.
fonte
Como está escrito, é uma função pura. Não produz efeitos colaterais. A função possui um parâmetro formal, mas possui duas entradas e sempre produzirá o mesmo valor para quaisquer duas entradas.
fonte
Como você observou apropriadamente, "isso pode me dar uma saída diferente amanhã" . Nesse caso, a resposta seria um retumbante "não" . Isto é especialmente verdade se o seu comportamento pretendido
dollarToEuro
foi corretamente interpretado como:No entanto, existe uma interpretação diferente, onde seria considerada pura:
dollarToEuro
diretamente acima é puro.Do ponto de vista da engenharia de software, é essencial declarar a dependência da
dollarToEuro
funçãofetchFromDatabase
. Portanto, refatorar a definição dadollarToEuro
seguinte maneira:Com esse resultado, dada a premissa de que
fetchFromDatabase
funciona satisfatoriamente, podemos concluir que a projeção defetchFromDatabase
ondollarToEuro
deve ser satisfatória. Ou a afirmação "fetchFromDatabase
é puro" implica quedollarToEuro
é puro (já quefetchFromDatabase
é uma base paradollarToEuro
o fator escalar dex
.Do post original, entendo que
fetchFromDatabase
é um tempo de função. Vamos melhorar o esforço de refatoração para tornar esse entendimento transparente e, portanto, claramente qualificadofetchFromDatabase
como uma função pura:fetchFromDatabase = (timestamp) => {/ * aqui vai a implementação * /};
Por fim, refataria o recurso da seguinte maneira:
Consequentemente,
dollarToEuro
pode ser testado em unidade simplesmente provando que chama corretamentefetchFromDatabase
(ou seu derivadoexchangeRate
).fonte
dollarToEuro
; Eu mencionei no OP que pode haver outros casos de uso. Eu escolhi dollarToEuro porque evoca instantaneamente o que estou tentando fazer, mas pode haver algo menos sutil que depende de uma variável livre que pode mudar, mas não necessariamente em função do tempo. Com isso em mente, considero o refator votado como o mais acessível e o que pode ajudar outras pessoas com casos de uso semelhantes. Obrigado pela sua ajuda, independentemente.Sou bilíngue do Haskell / JS e o Haskell é uma das línguas que mais preocupa a pureza das funções, então pensei em fornecer a perspectiva de como Haskell a vê.
Como outros já disseram, em Haskell, a leitura de uma variável mutável é geralmente considerada impura. Há uma diferença entre variáveis e definições , pois as variáveis podem mudar mais tarde, as definições são as mesmas para sempre. Portanto, se você o tivesse declarado
const
(supondo que seja apenas umnumber
e não possua estrutura interna mutável), ler a partir disso seria usar uma definição pura. Mas você queria modelar as taxas de câmbio alteradas ao longo do tempo, e isso requer algum tipo de mutabilidade e então você entra na impureza.Para descrever esses tipos de coisas impuras (podemos chamá-las de "efeitos" e seu uso "eficaz" em oposição a "puro") em Haskell, fazemos o que você pode chamar de metaprogramação . Hoje, a metaprogramação geralmente se refere a macros, o que não é o que quero dizer, mas apenas a idéia de escrever um programa para escrever outro programa em geral.
Nesse caso, em Haskell, escrevemos uma computação pura que calcula um programa eficaz que fará o que queremos. Portanto, o objetivo de um arquivo de origem Haskell (pelo menos um que descreva um programa, não uma biblioteca) é descrever uma computação pura para um programa eficaz que produz um vazio, chamado
main
. Em seguida, o trabalho do compilador Haskell é pegar esse arquivo de origem, executar a computação pura e colocar esse programa eficaz como um executável binário em algum lugar do disco rígido para ser executado posteriormente à sua vontade. Em outras palavras, existe uma lacuna entre o tempo em que a computação pura é executada (enquanto o compilador torna o executável) e o tempo em que o programa eficaz é executado (sempre que você executa o executável).Portanto, para nós, programas eficazes são realmente uma estrutura de dados e não fazem nada intrinsecamente apenas por serem mencionados (eles não têm * efeitos colaterais * além de seu valor de retorno; seu valor de retorno contém seus efeitos). Para um exemplo muito leve de uma classe TypeScript que descreve programas imutáveis e algumas coisas que você pode fazer com eles,
A chave é que, se você tiver um
Program<x>
, não ocorreram efeitos colaterais e essas são entidades totalmente funcionais e puras. Mapear uma função sobre um programa não tem efeitos colaterais, a menos que a função não seja pura; sequenciar dois programas não tem efeitos colaterais; etc.Portanto, por exemplo, de como aplicar isso no seu caso, você pode escrever algumas funções puras que retornam programas para obter usuários por ID e alterar um banco de dados e buscar dados JSON, como
e, em seguida, você pode descrever um trabalho cron para enrolar uma URL e procurar algum funcionário e notificar o supervisor de uma maneira puramente funcional, como
O ponto é que toda função aqui é uma função completamente pura; nada aconteceu até eu
action.run()
colocar em movimento. Além disso, eu posso escrever funções como,e se JS tivesse o cancelamento promissor, poderíamos ter dois programas correndo um contra o outro e pegar o primeiro resultado e cancelar o segundo. (Quero dizer, ainda podemos, mas fica menos claro o que fazer.)
Da mesma forma, no seu caso, podemos descrever a alteração das taxas de câmbio com
e
exchangeRate
poderia ser um programa que analisa um valor mutável,mas, mesmo assim, essa função
dollarsToEuros
agora é uma função pura de um número para um programa que produz um número, e você pode argumentar sobre isso dessa maneira equitativa determinística que pode argumentar sobre qualquer programa que não tenha efeitos colaterais.O custo, é claro, é que você deve eventualmente chamar isso em
.run()
algum lugar , e isso será impuro. Mas toda a estrutura do seu cálculo pode ser descrita por um cálculo puro, e você pode empurrar a impureza para as margens do seu código.fonte