Funções puras
Em desenvolvimento de software podemos separar as funções em dois grandes grupos: funções puras e impuras. Para que uma função seja considerada pura ela deve obedecer dois critérios: deve depender apenas de seus parâmetros de entrada e não deve causar efeitos colaterais.
Efeito colateral
Uma função possui efeito colateral (side effect) quando altera o estado fora do seu contexto local. Alguns exemplos de efeitos colaterais são:
- Alteração do estado (atributo) de um objeto;
- Chamada a um web service;
- Acesso a um banco de dados;
- Acesso ao sistema de arquivos;
- Impressão no console/terminal;
- Alteração de uma variável global.
Este são apenas alguns dos vários possíveis cenários que uma função com efeitos colaterais pode realizar.
Note que embora uma função sem efeito colateral não possa alterar o estado fora do seu contexto local, não significa que ela não possa consultar o estado externo. E o resultado da execução de uma função que não possui efeitos colaterais se limita ao valor de seu retorno. Logo, não faz sentido criarmos uma função sem efeito colateral que não tenha um retorno.
Funções puras
Podemos separar as funções sem efeito colateral em dois subgrupos: aquelas que dependem apenas dos parâmetros de entrada e aquelas que consultam informações fora do seu contexto local (podendo ser um atributo do objeto, uma variável global ou qualquer outro estado do sistema).
Este primeiro, que além de não causarem efeitos colaterais dependem apenas de seus parâmetros de entrada, compõe o grupo das funções que chamamos de puras.
Exemplos em pseudocódigos de funções puras:
// Dado 2 números, retornar o valor de sua soma.
somar(x, y) {
return x + y
}// Dada um conjunto de pessoas, retornar idade da mais velha.
obterMaiorIdade(Set<Pessoa> pessoas) {
idadePessoaMaisVelha = 0
for (Pessoa pessoa : pessoas) {
if (idadePessoaMaisVelha < pessoa.idade) {
idadePessoaMaisVelha = pessoa.idade
}
}
return idadePessoaMaisVelha
}
Existem formas mais elegantes de implementar a função obterMaiorIdade, mas optei por esta abordagem mais imperativa para mostrar que mesmo a variável idadePessoaMaisVelha sofrendo alterações (não é uma constante), ainda assim a função continua sendo pura, já que esta alteração fica limitada ao escopo da própria função.
Vantagens
Uma característica interessante das funções puras é que para uma determinada entrada, o retorno de sua execução é sempre o mesmo. Não importa a quantidade de vezes que obterMaiorIdade definida mais acima seja acionada, o valor do retorno sempre será o mesmo (desde que, claro, o conjunto de pessoas passadas como parâmetro permaneça o mesmo).
Isso facilita imensamente a execução de código em paralelo, já que uma função pura não consegue impactar outras funções que estejam sendo executadas naquele mesmo instante.
Outra característica das funções puras é que são muito fáceis de criar caches. Como sabemos que ao ser executada pela segunda vez com os mesmos parâmetros o retorno será igual ao anterior, não precisamos executar todos os passos novamente, bastando retornar o valor que foi calculado previamente. Em uma função impura que precise por exemplo fazer uma consulta em outro sistema, não seria possível fazer o cache desta forma, já que o resultado da segunda execução poderia ser diferente do resposta anterior.
Este tipo de função também facilita muito a criação de testes de unidade. Como o retorno de sua execução é previsível, fica muito fácil criar testes automatizados para validar as regras de negócio deste tipo de função. E por dependerem apenas dos parâmetros de entrada, podem ser executadas em paralelo, diminuindo o tempo necessário para validar a aplicação.
Limitações
Em algum momento será necessário introduzir efeitos colaterais em suas aplicações: seja para exibir alguma informação para a usuária, acesso ao banco de dados, interagir com sistema de arquivos, etc. O objetivo final de toda aplicação é produzir algum efeito colateral que seja perceptível para a usuária, logo, um sistema sem efeitos colaterais não tem propósito.
Mas não subestime o poder das funções puras! Elas são muito poderosas e mais abrangentes do que pode parecer. No passado era comum não existir uma fronteira bem definida entre as funções puras e impuras, fazendo com que funções impuras permeassem por todo sistema. Hoje existem padrões de projetos que auxiliam na separação clara entre elas e também técnicas que permitem minimizar ao máximo o número de funções impuras. Muitas linguagens, como scala e clojure, conseguem fazer este isolamento de forma bem eficiente. Outras, como Elm, Haskell e Idris, dão um passo além e praticamente toda aplicação é escrita utilizando apenas funções puras. Através de técnicas mais avançadas da programação funcional, estas linguagens conseguem encapsular e representar a intenção de um efeito em funções puras. Para entender melhor este processo teríamos que discutir sobre temas como functors, applicative functors, monads, … então isso vai ter que ficar para outros artigos.
Obrigado por ler até aqui! Se gostou deste texto, tenho mais alguns publicados em minha página do medium e também mantenho um podcast onde falo sobre livros que impactaram na minha carreira.