Qual é a melhor maneira de utilizar as funções puras?

3/13/2020 - DesenvolvimentoSoftware

O que são as funções puras?

As funções puras são caracterizadas por não produzir nenhum efeito colateral, ou seja, ela não muda qualquer estado na aplicação. Além disso, a função precisa ser completamente determinística, uma vez que cada saída será produzida para uma entrada específica.

A utilização:

Quando a função só gera um resultado determinístico e não muda de estado, o fluxo de operações é mais previsível e fica mais fácil depurar e testar o código, entender o seu funcionamento.

No contexto de desenvolvimento JavaScript, e mais ainda quando falamos do paradigma React + Redux (ou similares), o uso de funções puras é altamente recomendado para melhorar a manutenibilidade e escalabilidade do código e, principalmente, evitar bugs. 

Nesse artigo, você vai entender em mais detalhes o que é este conceito e porque devemos evitar ao máximo funções impuras. Vamos lá?

Mesmo input ⇒ mesmo output

Considere as seguintes funções:

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

add(2, 4); // 6

e

let x = 2;

const add = (y) => {

  x += y;

};

add(4); // x === 6 (the first time)

No primeiro caso, a função retorna um valor de acordo com os seus argumentos de entrada, independente de quando e onde ela é chamada. Sempre que passarmos 2 e 4 sabemos que o resultado será 6. Nada mais afeta o resultado.

A segunda função claramente depende de um estado compartilhado para produzir o resultado esperado. Ela altera uma variável que está fora do seu escopo, de forma que não podemos prever com certeza quais alterações em outras partes do código podem introduzir um bug no funcionamento desse trecho de código, afinal de contas o resultado da função é totalmente imprevisível.

O estado compartilhado cria uma dependência de tempo, o que significa que funções escritas dessa forma produzem resultados diferentes dependendo de quando são chamadas. Isso definitivamente não é o que queremos como desenvolvedores.

Olhando para as funções acima, qual das duas tem menos chances de produzir um bug esquisito que só ocorre em situações muito específicas? Qual das duas tem mais chances de funcionar normalmente num ambiente multi-thread que introduz paralelismo de ações?

Deve ficar bem claro que a primeira delas deve ser comportar melhor em ambas situações descritas acima.

Sem efeitos colaterais

Funções puras não podem fazer alterações que fogem do seu escopo. A ideia é que a função não deve fazer nada além de cálculos de manipulação de dados que lhe foram passados como entrada. Um checklist básico para evitar efeitos colaterais é o seguinte:

  1. Não modificar sua entrada
    1. Se fizemos alterações em alguma variável que foi passada como parâmetro para a função, teremos causado uma mutação que pode ser inesperada, e pode acabar comprometendo o funcionamento de outras funções do sistema.
  1. O ideal é sempre copiar o parâmetro de entrada e alterar essa cópia antes de retorná-la. Dessa forma evitamos alterações inesperadas em nossas variáveis.
  1. Não fazer chamadas HTTP ou de leitura de disco
    1. Nunca queremos misturar esse tipo de operação assíncrona com a lógica de manipulação de dados. Esse tipo de operação pode ter vários resultados diferentes inesperados, portanto devem ser devidamente modularizados e separados da parte lógica do código.
  1. OBS: É claro que a grande maioria dos sistemas precisa fazer chamadas de rede, leituras de disco e outras operações assíncronas. Isso não significa que há algo de errado com essas operações em si, mas é importante que elas sejam devidamente separadas em seus próprios módulos que possuem essa finalidade.
  1. Transparência Referencial
  1. Uma forma fácil de testar se sua função é pura seria verificar se substituir a chamada da função pelos seus valores de saída quebra o funcionamento do programa. Caso ele pare de funcionar, isso significa que sua função tem efeitos colaterais inesperados.

Alguns outros exemplos de efeitos colaterais antes de vermos um exemplo prático:

  • Mutação no input?
  • Chamadas de rede ou de disco
  • Queries no DOM
  • console.log (apesar de ser inofensivo, não deixa de ser um efeito colateral)
  • Inserções em bancos de dados
  • Acesso ao estado do sistema
  • Recebimento de input de usuário

Considere o seguinte trecho de código com a função impura impureAssoc:

const impureAssoc = (key, value, object) => {

  object[key] = value;

};

const person = {

  name: 'Bobo'

};

const result = impureAssoc('shoeSize', 400, person);

console.log({

  person, // { name: 'Bobo', shoeSize: 400 }

  result  // { name: 'Bobo', shoeSize: 400 }

});

Este é um exemplo de função com efeitos colaterais causados pela alteração do seu input. Visto que impureAssoc recebe um objeto e o altera, a variável person foi para sempre alterada devido à chamada da função, e isso não é imediatamente óbvio ao batermos o olho no código. 

Esse tipo de estado compartilhado significa que o impacto da função impureAssoc não é mais óbvio, não podemos considerar apenas seus inputs e outputs para entender o que ela alterou no sistema. Todas as variáveis que já entraram em contato com ela podem ter sido alteradas, causando funcionamentos inesperados no sistema.

Poderíamos purificar a função da seguinte forma:

const pureAssoc = (key, value, object) => ({

  ...object,

  [key]: value

});

const person = {

  name: 'Bobo'

};

const result = pureAssoc('shoeSize', 400, person);

console.log({

  person,

  result

});

Agora a função pureAssoc cria uma cópia do input, a altera e retorna este resultado. Dessa forma, nada fora da função é alterado e temos um trecho de código perfeitamente testável e previsível. Podemos utilizar essa função em qualquer contexto e mudar a ordem de sua chamada sem preocupações adicionais.

Conclusão

Tendo em vista todos os benefícios que foram apontados anteriormente, também é necessário ressaltar que não é realista escrever aplicações inteiras utilizando apenas funções puras! Afinal de contas os efeitos colaterais são justamente o que fazem o software ter um impacto real no mundo.

 O importante aqui é entender que sempre que o contexto permitir devemos escrever funções puras, de forma a tornar o nosso código mais legível, testável e modularizado. Os efeitos colaterais sempre existirão, o importante é que eles fiquem bem organizados em módulos diferentes e específicos para cada um. Se conseguirmos organizar o sistema dessa maneira, torna-se muito mais fácil encontrar erros e escalar a aplicação, pois a parte “pura” do sistema vai ser totalmente verificável através de testes unitários e será possível investigar individualmente os outros módulos.

E ai, Gostou do artigo? Venha aprender também sobre revisão de código

Referências principais

https://hackernoon.com/javascript-and-functional-programming-pt-3-pure-functions-d572bb52e21c

https://www.freecodecamp.org/news/what-is-a-pure-function-in-javascript-acb887375dfe/

https://medium.com/javascript-scene/master-the-javascript-interview-what-is-a-pure-function-d1c076bec976

Receba conteúdos sobre inovação digital, novas tecnologias, design e desenvolvimento.

Entre em contato

Telefones

+55 31 99291-5266

+1 650 691-5964

Endereço

R. Paraíba, 330, sala 1006

Belo Horizonte - MG - Brasil