Leia isso aqui se você não faz a mínima ideia do que é programação funcional
Olá!
Vamos começar com uma afirmação: a forma como a maioria das pessoas entende o ato de computar não é a única forma possível e existem diferenças fundamentais entre os paradigmas envolvidos, mas, que no final, fazem a mesma coisa.
Primeiro, vamos entender o que á máquina de Turing:
Ela é uma abstração de uma máquina de computar, que utiliza uma fita de memória que contém valores, um dispositivo que computa e um dispositivo de escrita/leitura para armazenar o resultado dessa operação. Essa é a ideia básica por trás de todo computador e deve ser relativamente intuitiva para quem já interagiu com algum computador alguma vez.
Expansões dessa mesma ideia primordial nos levam a abstrações no nível de hardware como a arquitetura de Von Neumann:
Bastante similar, não é mesmo?
A diferença incide entre as unidades especializadas em aritmética/lógica ou controle da CPU. Ai está a beleza de uma arquitetura bem pensada: ela é expansível. Exemplo: uma GPU é basicamente um outro elemento adicionado dentro dessa estrutura, sendo responsável por ser uma unidade de processamento especializada em cálculos gráficos (ou mineração de bitcoins…)
Porém, essa arquitetura tem um gargalo conhecido que gerou a criação da arquitetura Harvard, que tem como diferença fundamental a forma de se lidar com a memória.
Aqui vai um comparativo entre ambos:
Acredito que com esse conceito conseguimos entender o suficiente sobre o ato de computar de forma “tradicional”.
Mas o que acontece quando mudamos a premissa de tudo isso? E se ao invés da máquina de Turing, usássemos outra abstração inicial a partir de tudo isso?
Enter λ Calculus.
Sim, o símbolo é o mesmo do logo do jogo Half-Life e do produto AWS Lambda. Após a explicação talvez fique mais claro o porquê.
O cálculo lambda foi proposto por Alonzo Church na década de 1930, usa a seguinte premissa: a unidade fundamental de cálculo é uma função matemática, ou seja, o resultado será sempre o mesmo dado um determinado parâmetro de entrada, sem efeitos colaterais em sua computação.
As regras básicas são as seguintes:
Com esse conjunto de regras, permitimos que os seguintes conceitos existam: imutabilidade, funções como cidadãs de primeira classe e ausência de loops.
A título de curiosidade, o cálculo lambda foi posteriormente validada como compatível com a máquina de Turing em 1937 pelo próprio Alan Turing, ou seja, o que é possível calcular com um, também é possível com o outro. Esse conceito de “técnica para validar se um mecanismo consegue ser um mecanismo de computação universal”, chamamos de Turing Complete.
Devido a essa característica, conseguimos usar linguagens funcionais nas arquiteturas de hardware mencionadas anteriormente sem ter que usar um computador específico para esse paradigma.
Bom, se o próprio Turing disse que é possível, posso afirmar que:
A partir daqui um conhecimento básico em programação estruturada e/ou orientação a objetos se faz necessário para melhor aproveitamento.
Pensa em tudo que você conhece sobre programação comum: como módulos, namespaces, arrays, testes, variáveis, métodos, classes, ifs e iterações.
Tudo isso existe dentro do paradigma funcional da mesma forma, porém, removendo o que é ruim: efeitos colaterais.
A forma mais simples que eu consigo explicar o conceito de imutabilidade é: toda variável é declarada somente um vez e todos os consequentes acessos serão somente leitura. Caso seja necessário reatribuir o valor naquela variável, você terá o compilador te avisando que não é possível e irá te obrigar a criar uma cópia daquela variável com outro nome.
Somente essa ideia simples te obriga a pensar orientado a funções puras, ou seja, cada funcionalidade do seu código terá lazy evalution, transparência referencial, otimizações (dado a ausência de loops) e o mais importante: a garantia que aquele código só faz aquilo que diz, sem comportamentos imprevisíveis. Sem contar que é mais fácil de testar também!
Além disso, no paradigma funcional, funções são cidadãs de primeira classe, isso significa que funções possuem um suporte muito bom, permitindo funcionalidades como currying (em JS) e pipe operators (em Elixir). Nesse ponto, o principal ganho percebido é que a legibilidade fica muito melhor e é possível fazer muito mais com menos código. Isso torna seu código mais eficiente.
Por fim, em linguagens funcionais, loops não são recomendados (justamente por incentivar efeitos colaterais, como contadores e acumuladores), portanto, uma abordagem mais adequada é recursividade ou funções anônimas. Geralmente, substitui-se Loops com IFs por funções Map/Filter e Loops com acumuladores por funções Reduce.
Para finalizar, vamos comparar a solução do seguinte problema: contar quantas vogais existem na palavra “abracadabra” usando uma linguagem OO, como C# e a solução para o mesmo problema em uma linguagem funcional, como Elixir.
Solução em C#:
Solução usando Elixir com uso de função filter, função anônima, pipe operator e imutabilidade:
Em ambos as soluções, o resultado é 5.
O que achou?
No próximo artigo, vou detalhar um pouco melhor a experiência de se aprender paradigma funcional após 10 anos trabalhando com orientação a objetos.
Até!
Você gostou do conteúdo e gostaria de fazer mentoria comigo? Clique aqui e descubra como.