Isso é uma função pura?


117

A maioria das fontes define uma função pura como tendo as duas propriedades a seguir:

  1. Seu valor de retorno é o mesmo para os mesmos argumentos.
  2. 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 constna primeira linha. Utilizado letanteriormente 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?


6
Pureness de tal linguagem dinâmica como JS é um tema muito complicado:function myNumber(n) { this.n = n; }; myNumber.prototype.valueOf = function() { console.log('impure'); return this.n; }; const n = new myNumber(42); add(n, 1);
zerkms

29
Pureza significa que você pode substituir a chamada de função pelo valor do resultado no nível do código sem alterar o comportamento do seu programa.
bob

11
Para ir um pouco mais longe sobre o que constitui um efeito colateral, e com mais terminologia teórica, consulte cs.stackexchange.com/questions/116377/…
Gilles 'SO- deixa de ser mau'

3
Hoje, a função é (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 porque exchangeRatenão pode mudar.
user253751

2
Esta é uma função impuro, Se você quiser torná-lo puro, você pode usar 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 que
Vikash Tiwari

Respostas:


133

O dollarToEurovalor de retorno do depende de uma variável externa que não é um argumento; portanto, a função é impura.

Na resposta é NÃO, como então podemos refatorar a função para ser pura?

Uma opção é passar exchangeRate. Dessa forma, toda vez que os argumentos são (something, somethingElse), é garantido que a saída seja something * somethingElse:

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

Observe que, para a programação funcional, você deve evitar let- sempre use constpara evitar a reatribuição.


6
Não ter variáveis ​​livres não é um requisito para uma função ser pura: const add = x => y => x + y; const one = add(42);aqui ambas adde onesão funções puras.
Zerkms 7/11

7
const foo = 42; const add42 = x => x + foo;<- essa é outra função pura, que novamente usa variáveis ​​livres.
Zerkms 7/11

8
@zerkms - Eu gostaria muito de ver sua resposta a essa pergunta (mesmo que apenas reformule o CertainPerformance para usar terminologia diferente). Eu não acho que seria duplicado, e seria esclarecedor, principalmente quando citado (idealmente com fontes melhores do que o artigo da Wikipedia acima, mas se isso é tudo o que conseguimos, ainda ganha). (Seria fácil de ler este comentário em algum tipo de luz negativa Confie em mim que estou sendo verdadeira, eu acho que essa resposta iria ser grande e gostaria de lê-lo..)
TJ Crowder

17
Eu acho que você e o @zerkms estão errados. Você parece achar que a dollarToEurofunção no exemplo da sua resposta é impura porque depende da variável livre exchangeRate. 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 a dollarToEurofunção é impura porque depende da exchangeRateorigem de um banco de dados. Ele diz que é impuro porque "depende do pedido de informação transitivamente".
Aadit M Shah

9
(cont) Novamente, isso é um absurdo porque sugere que dollarToEuroé impuro porque exchangeRateé uma variável livre. Sugere que se exchangeRatenão fosse uma variável livre, ou seja, se fosse um argumento, dollarToEuroseria puro. Por isso, sugere que dollarToEuro(100)é impuro, mas dollarToEuro(100, exchangeRate)é puro. Isso é claramente absurdo, porque nos dois casos você depende do exchangeRateque vem de um banco de dados. A única diferença é se é ou não exchangeRateuma variável livre dentro da dollarToEurofunção.
Aadit M Shah

76

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 de eax", 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.

const fib = (() => {
    const memo = [0, 1];

    return n => {
      if (n >= memo.length) memo[n] = fib(n - 1) + fib(n - 2);
      return memo[n];
    };
})();

console.log(fib(100));

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.

const greet = name => {
    console.log("Hello %s!", name);
};

greet("World");
greet("Snowman");

A greetfunçã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 exemplo Hello 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.

console.log("Hello %s!", "World");
console.log("Hello %s!", "Snowman");

Aqui, destacamos a definição de greet e não mudou a semântica do programa.

Agora, considere o seguinte programa.

undefined;
undefined;

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 a greetfunção é impura. Não é referencialmente transparente.

Agora, vamos considerar outro exemplo. Considere o seguinte programa.

const main = async () => {
    const response = await fetch("https://time.akamai.com/");
    const serverTime = 1000 * await response.json();
    const timeDiff = time => time - serverTime;
    console.log("%d ms", timeDiff(Date.now()));
};

main();

Claramente, a mainfunçã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 dollarToEurofunção no exemplo a seguir é impura porque "depende do IO transitivamente".

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

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. É chamadounsafePerformIO 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 usarmosunsafePerformIO , 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.

let exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => {
  return x * exchangeRate;
};

dollarToEuro(100) //90 today

dollarToEuro(100) //something else tomorrow

Aqui, estou assumindo que, porque exchangeRatenão está definido como const, 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 exchangeRatevariável não for modificada e nunca será modificada no futuro (ou seja, se for um valor constante), mesmo que seja definida como let, 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 timeDiffexemplo várias vezes, obterá valores diferentes para serverTimee, portanto, resultados diferentes. No entanto, como o valor de serverTimenunca muda enquanto o programa está sendo executado, a timeDifffunção é pura.


3
Isso foi muito informativo. Obrigado. E eu pretendia usar constno meu exemplo.
Snowman

3
Se você pretendia usar const, a dollarToEurofunção é realmente pura. A única maneira de exchangeRatemudar 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.
Aadit M Shah

3
Isso soa como uma pequena teoria sobre a relatividade: as constantes são apenas relativamente constantes, não absolutamente, ou seja, relativas ao processo em execução. Claramente, a única resposta certa aqui. +1.
bob

5
Não concordo com "é impuro porque, eventualmente, compila instruções como" mover esse valor para eax "e" adicionar esse valor ao conteúdo de eax " . Se eaxfor 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..
3Dave

3
@ Bergi: Na verdade, em uma linguagem pura e com valores imutáveis, a identidade é irrelevante. Se duas referências avaliadas para o mesmo valor são duas para o mesmo objeto ou para objetos diferentes, pode ser observado alterando o objeto por uma das referências e observando se o valor também muda quando recuperado por outra referência. Sem mutação, a identidade se torna irrelevante. (Como Rich Hickey diria: Identidade é uma série de Estados longo do tempo.)
Jörg W Mittag

23

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:

const add = (x, y) => x + y;

function myNumber(n) { this.n = n; };
myNumber.prototype.valueOf = function() {
    console.log('impure'); return this.n;
};

const n = new myNumber(42);

add(n, 1); // this call produces a side effect

Uma resposta do meu pragmático:

De própria definição da wikipedia

Na programação de computadores, uma função pura é uma função que possui as seguintes propriedades:

  1. Seu valor de retorno é o mesmo para os mesmos argumentos (nenhuma variação com variáveis ​​estáticas locais, variáveis ​​não locais, argumentos de referência mutáveis ​​ou fluxos de entrada de dispositivos de E / S).
  2. Sua avaliação não tem efeitos colaterais (nenhuma mutação de variáveis ​​estáticas locais, variáveis ​​não locais, argumentos de referência mutáveis ​​ou fluxos de E / S).

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:

const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

É 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:


4
@TJCrowder mecomo zerkms que fornece uma resposta.
Zerkms 7/11

2
Sim, com Javascript é tudo sobre confiança, não garante
bob

4
@bob ... ou é uma chamada bloqueada.
Zerkms 7/11

11
@zerkms - Obrigado. Só para ter 100% de certeza, a principal diferença entre você add42e minha addXé puramente que minha xpode ser alterada e sua ftnão pode ser alterada (e, portanto, add42o valor de retorno da variável não varia com base ft)?
TJ Crowder #

5
Não concordo que a dollarToEurofunção no seu exemplo seja impura. Expliquei por que discordo da minha resposta. stackoverflow.com/a/58749249/783743
Aadit M Shah

14

Como outras respostas disseram, da maneira que você implementou dollarToEuro,

let exchangeRate = fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => { return x * exchangeRate; }; 

é realmente puro, porque a taxa de câmbio não é atualizada enquanto o programa está sendo executado. Conceitualmente, no entanto, dollarToEuroparece 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 implementaram dollarToEuromas dollarToEuroAtInstantOfProgramStart.

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 dollarToEuroforneceria 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 quantidade de dinheiro para trocar
  • Uma autoridade histórica para consultar sobre taxas de câmbio
  • A data em que a transação ocorreu (para indexar a autoridade histórica)

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:

function dollarToEuro(x, authority, date) {
    const exchangeRate = authority(date);
    return x * exchangeRate;
}

dollarToEuro(100, fetchFromDatabase, Date.now());

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 dodollarToEuro 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:

function dollarToEuro(x, date) {
    const exchangeRate = fetchFromDatabase(date);
    return x * exchangeRate;
}

dollarToEuro(100, Date.now());

@Snowman De nada! Atualizei a resposta um pouco para adicionar mais exemplos de código.
TheHansinator 8/11

8

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:

let exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;

const dollarToEuro = (x) => {
  return x * exchangeRate;
};

dollarToEuro(100) //90 today

dollarToEuro(100) //something else tomorrow

Se exchangeRatenunca puder ser modificado entre as duas chamadas para dollarToEuro(100), é possível memorizar o resultado da primeira chamada dollarToEuro(100)e otimizar a segunda chamada. O resultado será o mesmo, para que possamos lembrar o valor anterior.

A exchangeRatepode 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 procura exchangeRateuma 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.


8

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 exchangeRatecomo argumento.

Isso satisfaria as duas condições.

  1. Sempre retornaria o mesmo valor ao transmitir o mesmo valor e taxa de câmbio.
  2. Também não teria efeitos colaterais.

Código de exemplo:

const dollarToEuro = (x, exchangeRate) => {
  return x * exchangeRate;
};

dollarToEuro(100, fetchFromDatabase())

11
"o que quase definitivamente vai mudar" --- não é, é const.
Zerkms 9/11/19

7

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):

function f1(x, y) {
  if (Math.random() > 0.5) { return x; }
  return y;
}

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 chamada f1("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 para f1não são referencialmente transparentes, portanto, f1são impuras.

Outra maneira pela qual uma chamada de função pode ter semântica diferente de um valor é executando instruções. Por exemplo:

function f2(x) {
  console.log("foo");
  return x;
}

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, f2impuro.

Se a sua dollarToEurofunção é referencialmente transparente (e, portanto, pura) depende de duas coisas:

  • O 'escopo' do que consideramos referencialmente transparente
  • Se a exchangeRatemudanç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 exchangeRateestivesse usando var, ele poderia mudar entre cada chamada para dollarToEuro; 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 retorno dollarToEuroaté 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.


4

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.


2

Podemos chamar essas funções de funções puras. Se a resposta for NÃO, como então podemos refatorá-la para ser uma?

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 dollarToEurofoi corretamente interpretado como:

const dollarToEuro = (x) => {
  const exchangeRate =  fetchFromDatabase(); // evaluates to say 0.9 for today;
  return x * exchangeRate;
};

No entanto, existe uma interpretação diferente, onde seria considerada pura:

const dollarToEuro = ( () => {
    const exchangeRate =  fetchFromDatabase();

    return ( x ) => x * exchangeRate;
} )();

dollarToEuro diretamente acima é puro.


Do ponto de vista da engenharia de software, é essencial declarar a dependência da dollarToEurofunção fetchFromDatabase. Portanto, refatorar a definição da dollarToEuroseguinte maneira:

const dollarToEuro = ( x, fetchFromDatabase ) => {
  return x * fetchFromDatabase();
};

Com esse resultado, dada a premissa de que fetchFromDatabasefunciona satisfatoriamente, podemos concluir que a projeção de fetchFromDatabaseon dollarToEurodeve ser satisfatória. Ou a afirmação " fetchFromDatabaseé puro" implica que dollarToEuroé puro (já que fetchFromDatabaseé uma base para dollarToEuroo fator escalar de x.

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 qualificado fetchFromDatabasecomo uma função pura:

fetchFromDatabase = (timestamp) => {/ * aqui vai a implementação * /};

Por fim, refataria o recurso da seguinte maneira:

const fetchFromDatabase = ( timestamp ) => { /* here goes the implementation */ };

// Do a partial application of `fetchFromDatabase` 
const exchangeRate = fetchFromDatabase.bind( null, Date.now() );

const dollarToEuro = ( dollarAmount, exchangeRate ) => dollarAmount * exchangeRate();

Consequentemente, dollarToEuropode ser testado em unidade simplesmente provando que chama corretamente fetchFromDatabase(ou seu derivado exchangeRate).


11
Isso foi muito esclarecedor. +1. Obrigado.
Snowman

Embora eu ache sua resposta mais informativa, e talvez a melhor refatoração para o caso de uso específico de 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.
Snowman

-1

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 um numbere 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,

export class Program<x> {
   // wrapped function value
   constructor(public run: () => Promise<x>) {}
   // promotion of any value into a program which makes that value
   static of<v>(value: v): Program<v> {
     return new Program(() => Promise.resolve(value));
   }
   // applying any pure function to a program which makes its input
   map<y>(fn: (x: x) => y): Program<y> {
     return new Program(() => this.run().then(fn));
   }
   // sequencing two programs together
   chain<y>(after: (x: x) => Program<y>): Program<y> {
    return new Program(() => this.run().then(x => after(x).run()));
   }
}

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

// assuming a database library in knex, say
function getUserById(id: number): Program<{ id: number, name: string, supervisor_id: number }> {
    return new Program(() => knex.select('*').from('users').where({ id }));
}
function notifyUserById(id: number, message: string): Program<void> {
    return new Program(() => knex('messages').insert({ user_id: id, type: 'notification', message }));
}
function fetchJSON(url: string): Program<any> {
  return new Program(() => fetch(url).then(response => response.json()));
}

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

const action =
  fetchJSON('http://myapi.example.com/employee-of-the-month')
    .chain(eotmInfo => getUserById(eotmInfo.id))
    .chain(employee => 
        getUserById(employee.supervisor_id)
          .chain(supervisor => notifyUserById(
            supervisor.id,
            'Your subordinate ' + employee.name + ' is employee of the month!'
          ))
    );

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,

// do two things in parallel
function parallel<x, y>(x: Program<x>, y: Program<y>): Program<[x, y]> {
    return new Program(() => Promise.all([x.run(), y.run()]));
}

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

declare const exchangeRate: Program<number>;

function dollarsToEuros(dollars: number): Program<number> {
  return exchangeRate.map(rate => dollars * rate);
}

e exchangeRatepoderia ser um programa que analisa um valor mutável,

let privateExchangeRate: number = 0;
export function setExchangeRate(value: number): Program<void> {
  return new Program(() => { privateExchangeRate = value; return Promise.resolve(undefined); });
}
export const exchangeRate: Program<number> = new Program(() => {
  return Promise.resolve(privateExchangeRate); 
});

mas, mesmo assim, essa função dollarsToEurosagora é 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.


Estou curioso para saber por que isso continua com o voto negativo, mas quero dizer que ainda o sustento (é, de fato, como você manipula programas em Haskell onde as coisas são puras por padrão) e terá o prazer de registrar os votos negativos. Ainda assim, se os votantes negativos quiseram deixar comentários explicando o que não gostam, posso tentar melhorá-lo.
CR Drost

Sim, eu queria saber por que existem tantos votos negativos, mas não um único comentário, além, é claro, do autor.
Buda Örs
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.