Ultimamente, as linguagens ditas funcionais estão ganhando cada vez mais a atenção dos desenvolvedores em geral. Já não é tão difícil encontrarmos projetos que utilizem uma linguagem funcional para resolver problemas que envolvem principalmente paralelismo e/ou processamento de quantidades grandes de dados. As ideias fundamentadas pelo paradigma funcional dão essa “vocação” para o paralelismo e processamento de quantidades extensas de dados para estas linguagens ditas funcionais. Mas, no final das contas, o que torna as linguagens funcionais tão adequadas para a resolução destes tipos de problemas? E quais são os contrapontos entre o paradigma funcional e o paradigma orientado a objetos? Será que o paradigma funcional pode substituir completamente o paradigma orientado a objetos?
O que é o paradigma funcional?
Antes de qualquer coisa: não, o paradigma funcional não tem esse nome porque ele funciona.
O paradigma funcional possui esse nome porque ele está fortemente fundamentado nos conceitos das funções matemáticas. Mas calma! Muita gente já “torce o nariz” ao ouvir falar de matemática, mas os conceitos envolvidos na descrição do paradigma funcional podem ser mais tranquilos de se entender do que você imagina.
Relembrando alguns conceitos de funções matemáticas
Vamos recapitular um pouco sobre uma função matemática. Uma função matemática tem essa aparência:
Se verificarmos, uma função matemática possui uma única entrada. Em uma função matemática, essa única entrada sofre um tipo de processamento, processamento este que retorna uma saída de qualquer maneira. No caso da função acima:
- A entrada é definida por
x
; - O processamento é a elevação de
x
ao quadrado mais3
; - A saída é o resultado do processamento descrito acima.
Mais um exemplo: se entrarmos com o número 2
nessa função matemática, teremos a saída 7
, pois 2^2 (4) + 3
é igual a 7
.
Existem alguns detalhes importantes. A função acima, além de sempre retornar um resultado compulsoriamente, ela sempre retornará a mesma saída para uma determinada entrada. Por exemplo: podemos “chamar” esta função passando para ela a entrada 2
hoje, amanhã ou daqui 10 anos… Não importa: o resultado dela sempre será 7
, independente da situação. Nós chamamos esta característica de determinismo. Por isso, dizemos que funções matemáticas em sua essência são determinísticas.
Outro fato interessante: funções matemáticas não possuem o conceito de escopo. Isso quer dizer que a entrada é processada e a saída correspondente é devolvida imediatamente. Não existem “variáveis” intermediárias que só podem ser acessadas dentro da função matemática: só existe a entrada e a saída.
Curso Java - Fundamentos
Conhecer o cursoTrazendo conceitos de funções matemáticas para as linguagens de programação
O paradigma funcional se baseia fortemente nos conceitos de funções matemáticas que discutimos no tópico anterior. E, sendo um paradigma, não necessariamente precisa ser implementado com uma linguagem funcional… Por exemplo, poderíamos reescrever a função matemática anterior com o seguinte código Java:
public int doFunction(int x){
return (x * x) + 3;
}
System.out.println(doFunction(2)); // A saída será "7"!
Veja que, na função acima:
- Temos uma única entrada;
- Não existe o conceito de escopo, pelo menos não explicitamente (já que não existem variáveis criadas dentro do método
doFunction()
); - O método sempre irá produzir uma saída;
- O método é determinístico, pois ele irá sempre produzir a mesma saída para uma mesma entrada.
Sendo assim, conseguimos criar um código funcional, mesmo utilizando o Java, que não é uma linguagem essencialmente funcional. Acima, ainda temos uma outra característica intrínseca ao paradigma funcional que é fantástica: a imutabilidade! Veja que, em nossa versão Java da função matemática, a entrada nunca é modificada: ela simplesmente sofre um processamento, produzindo uma saída. A saída produzida também nunca é modificada… Isso é fantástico, pois ao final, estamos quase que trabalhando com constantes o tempo inteiro. Obviamente, o Java e outras linguagens majoritariamente orientadas a objeto não dão um suporte 100% para estes conceitos que descrevemos… Porém, se pegarmos linguagens majoritariamente funcionais, nós teremos estes princípios básicos levados mais à risca. Por exemplo: vamos considerar a reescrita desta função em F#, uma linguagen funcional:
let doFunction x = (x * x) + 3
let result = doFunction 2
printfn "Resultado: %i" result
Acima, temos a mesma função reescrita em uma linguagen funcional. Veja que os conceitos de ausência de escopo, massa de entrada e saída e determinismo são respeitados aqui. Aliás, se você tentar modificar a “variável” result
(que aqui é chamada de binding), o compilador irá produzir um erro.
Por que linguagens funcionais podem ser interessantes para paralelismo, assincronia e processamento de grandes quantidades de informações?
Nós estamos, em maioria, acostumados a escrever código orientado a objetos, e uma das premissas da orientação a objetos é a transição de estado: nós criamos variáveis e/ou objetos que mudam de conteúdo o tempo inteiro, fazendo com que o nosso código tome decisões em cima destas flutuações de estado que acontecem. Isso não existe dentro do paradigma funcional, tendo em vista o determinismo e a imutabilidade. Quando estamos falando sobre o processamento de quantidades muito grandes de dados, tornar esse processamento assíncrono e paralelo é essencial. E isso pode se tornar um problema muito complicado quando falamos de transição de estado neste cenário. Um exemplo simples: quando criamos threads, uma thread não pode acessar o conteúdo que está sendo processado por outra thread. Quando precisamos que threads por alguma razão compartilhem alguma varíavel, nós acabamos precisando recorrer a patterns (como o famigerado singleton ). E no final, nós ainda precisamos nos preocupar em garantir que a modificação destes recursos compartilhados não irá ocorrer de maneira concorrente, com uma thread sobrescrevendo ou passando por cima do resultado de uma outra thread ao mesmo tempo. Para isso, precisamos recorrer a recursos como semáforos e monitores. Veja que, somente por termos um código baseado em transições de estados, nós precisamos recorrer a uma certa complexidade de código para garantir que tudo ocorra bem. Logicamente isso é factível, mas é extremamente trabalhoso. Linguagens funcionais não possuem estes problemas, pois elas já trabalham com imutabilidade e determinismo nativamente. Imutabilidade e determinismo são pilares do paradigma funcional. Se não existem transições de estado, nós não teríamos este tipo de problema se utilizássemos uma linguagem funcional.
Curso C# (C Sharp) Básico
Conhecer o cursoEntão devo chutar a orientação a objetos e aprender somente o paradigma funcional?
Não! Não faça isso! O paradigma funcional também possui seus problemas… A curva de aprendizado tende a ser ligeiramente maior do que a curva do paradigma orientado a objetos, principalmente porque muitas pessoas já criam um “bloqueio” pela associação à matemática. Existem também alguns conceitos dentro do paradigma funcional que não são tão “naturais” de serem entendidos, como currying e monads. Sendo assim, a curva de aprendizado tende a ser um pouquinho mais íngreme. Também temos o fato de que pouca coisa no mundo real é determinística: estamos falando das típicas situações de I/O. O acesso a uma tabela de um banco de dados, por exemplo, não pode ser determinístico: nós podemos tanto conseguir acessar a tabela como podemos em algum momento ter uma queda de conexão… Logicamente, as linguagens funcionais modernas são preparadas para também lidar com situações não-determinísticas, mas cada uma delas pode tratar de maneira diferente, o que contribui para aumentar um pouco mais o processo de aprendizagem e aplicação correta em situações do dia-a-dia. Existe também a questão de legibilidade do código. Eu particularmente observo que é mais fácil produzir código ilegível em linguagens funcionais do que em linguagens orientadas a objeto. Tudo bem, um desenvolvedor iniciante (ou um desenvolvedor infelizmente ruim) irá produzir código ruim de qualquer maneira… Mas observo que as linguagens funcionais dão uma “forcinha” a mais nesse sentido por diferentes fatores: sintaxe propriamente dita das linguagens que implementam este paradigma, a maior ocorrência da não-observância dos pilares funcionais, entre outros. Por isso, pelo menos para mim, o ideal é: aprenda e domine os dois paradigmas. Ambos serão muito úteis, dependendo-se da situação. Precisa criar uma página de cadastro de algo? Provavelmente a orientação a objetos pode ser mais aplicável… Precisa gerar um CSV a partir de um conjunto grande de dados? Provavelmente uma linguagem ou até mesmo uma abordagem funcional será mais aplicável. Aqui vale o velho mantra: não existe bala de prata no mundo de desenvolvimento de software. Aprenda os dois paradigmas e seja feliz!