
As funções que não são puras
Tempo de leitura: 3 minutos
A cada dia, mais desenvolvedores tomam conhecimento do paradigma da programação funcional. Esse paradigma promete um código eficiente e livre de bugs, pois as funções puras são mais fáceis de testar e paralelizar.
Na prática, applications funcionais completas ainda são algo abstrato. No entanto, certos concepts do paradigma da programação funcional são cada vez mais aplicados a linguagens não funcionais. Além disso, essa abordagem “parcialmente funcional” ajuda a resolver muitos “problemas” comuns de maneira mais eficaz.
Hoje vamos examinar mais de perto um desses conceitos funcionais: as funções puras e impuras.
NOTA: Usaremos pseudocódigo baseado em Kotlin para definir nossas funções, mantendo os exemplos simples para que sejam mais fáceis de acompanhar.
Funções puras
A função pura é uma função que não apresenta efeitos colaterais — em outras palavras, essa função não deve recuperar nem modificar nenhum valor além daqueles passados como parâmetros. Dessa forma, cada chamada à função com os mesmos argumentos sempre resultará no mesmo resultado (valor retornado). Vamos começar definindo uma função pura:
fun max(a: Int, b: Int) {
if (a > b)
return a
else
return b
}
A max função recebe dois argumentos, dois números, e retorna o maior deles. Observe que essa função não acessa nem modifica nenhum valor fora do escopo da função; portanto, é uma função pura. Podemos ter certeza de que chamar essa função com os argumentos 2 e 7 SEMPRE retornará 7.
Para entender melhor esse conceito, vamos examinar mais de perto o outro lado da moeda — as várias maneiras de quebrar a pureza das funções.
Funções Impuras
Uma função impura é uma função que possui efeitos colaterais — ela modifica ou acessa valores fora do escopo da função (fora do corpo da função).
Efeitos colaterais bastante óbvios
O exemplo mais simples é uma função que modifica uma propriedade externa para armazenar um estado.
val loalScore = 0
val getScore(score: Int): Int {
loalScore = score
return loalScore
}Nesse caso, a impureza não se manifesta de forma significativa, pois as chamadas de método subsequentes retornarão o mesmo valor:
getScore(12) // returns 12
getScore(6) // returns 6
getScore(3) // returns 3Essa função modifica um valor externo, mas ainda assim retorna o mesmo valor a cada chamada devido à atribuição de valor. No entanto, nem sempre é assim. Vamos considerar outra função impura:
val addScore = 0
val addScore(score: Int): Int {
loalScore + score
return loalScore
}Essa função apresenta “impureza mais forte”, pois modifica um valor externo e retorna um resultado diferente a cada chamada — o estado é armazenado na variável, fora do escopo da função:
addScore(12) // returns 12
addScore(6) // returns 18
addScore(3) // returns 21A desvantagem de manter o estado é que, às vezes, os testes se tornam mais complexos; no entanto, não podemos evitar isso em muitas Applications.
A função a seguir está acessando um valor fora do escopo da função, o que a torna impura:
fun getString(length: Int): String {
return Random().nextString(length)
}Desta vez, cada chamada à função com o mesmo argumento resultará no retorno de um valor diferente:
getString(2) // returns "ab"
getString(2) // returns "hh"
getString(2) // returns "zk"Embora os efeitos colaterais apresentados anteriormente sejam bastante fáceis de identificar, muitas vezes os efeitos colaterais podem ser mais sutis:
Efeitos colaterais não tão óbvios
Um cenário interessante de efeitos colaterais é a modificação do objeto passado como argumento de uma função:
fun increaseHeight(person: Person) {
person.height++
}Chamar essa função várias vezes com a mesma Person instância resultará em saídas diferentes, pois o valor fora da função (armazenado na instância de Person) é modificado.
Uma exceção lançada por uma função é um excelente exemplo dos efeitos colaterais que são mais difíceis de identificar:
fun addDistance (a:Int, b:Int): Int {
if(a < 0) {
throw IllegalAccessException("a must be >= 0")
}
return a + b
}
Outra maneira interessante de criar efeitos colaterais é simplesmente chamando outra função que produza efeitos colaterais:
fun firstFunction() {
addDistance(-5, 7)
}
fun addDistance (a:Int, b:Int): Int {
if(a < 0) {
throw IllegalAccessException("a must be >= 0")
}
return a + b
}
Outro efeito colateral não tão óbvio é o registro em log. Vamos dar uma olhada neste exemplo real do nosso Video Chat :
private PublisherKit.PublisherListener publisherListener = new PublisherKit.PublisherListener() {
@Override
public void onStreamCreated(PublisherKit publisherKit, Stream stream) {
Log.d(TAG, "onStreamCreated: Publisher Stream Created. Own stream " + stream.getStreamId());
}
@Override
public void onStreamDestroyed(PublisherKit publisherKit, Stream stream) {
Log.d(TAG, "onStreamDestroyed: Publisher Stream Destroyed. Own stream " + stream.getStreamId());
}
@Override
public void onError(PublisherKit publisherKit, OpentokError opentokError) {
Log.d(TAG, "PublisherKit onError: " + opentokError.getMessage());
}
};No código acima, o registro em log, como efeito colateral, não afeta a lógica da aplicação, mas nos ajuda a entender o que está acontecendo nela. Posteriormente, fica fácil para o desenvolvedor utilizar os dados retornados pelas chamadas de retorno e introduzir mais efeitos colaterais.
Determinar se uma função é pura ou impura
Existem dois indícios de que uma função pode ser impura: ela não recebe nenhum argumento nem retorna nenhum valor. Vejamos o primeiro caso:
list.getItem(): StringNo exemplo acima, a função não recebe nenhum parâmetro, mas retorna um valor. Isso significa que, muito provavelmente, o valor é obtido do estado da classe. Vamos analisar o que acontece quando uma função não retorna nenhum valor:
list.setItem("item")Ao observar o nome da função, podemos perceber que o parâmetro provavelmente será usado para modificar o estado da classe.
E, por fim, podemos ter uma combinação em que não há argumento nem valor retornado:
list.sort()
Essas são apenas pistas. Nem sempre é assim, mas essas pistas costumam ser bons indicadores de pureza.
Resumo
No paradigma da programação funcional, idealmente, todas as funções são puras.
No entanto, em muitas aplicações do mundo real, as coisas não são exatamente tão binárias assim. Às vezes, não é possível evitar funções impuras, especialmente se uma aplicação exigir recursos externos, como persistência, entrada do usuário ou acesso a dados de rede. A presença desses recursos quebra a pureza da função e de toda a aplicação, o que não é necessariamente ruim.
Normalmente, temos uma mistura de funções puras e impuras em um único aplicativo. É uma boa prática estar atento à pureza e à impureza, pois isso facilita os testes do aplicativo e nos ajuda a evitar bugs.