Programação Funcional — Guia de Introdução

Henrique F. Teixeira
@truehenrique
Published in
15 min readMar 27, 2018

Este artigo tem o objetivo de ser uma referência para quem está estudando ou deseja começar a estudar programação funcional.

Através dele, procuro mostrar as principais bases do paradigma funcional relacionando com os paradigmas mais tradicionais, como estruturado e orientado a objetos.

O artigo foi separado em seções específicas que podem ser lidas individualmente:

  1. Funções
  2. Estilo declarativo
  3. Imutabilidade
  4. Pureza
  5. Tipagem e Estruturas
  6. Pensamento OO vs Pensamento Funcional
  7. Conceitos e principios funcionais utilizando Ruby
  8. Links Externos e Referências

Introdução

Ultimamente um antigo paradigma de programação tem voltado a tona em diversos fóruns e discussões mundo a volta. Linguagens como Haskell, Elixir e Scala aparecem em constante ascensão, sendo adotadas por grandes empresas (Linkedin, Pinterest, Twitter, NuBank e etc…). A programação funcional nos trás benefícios que tornam o trabalho com threads, concorrência e paralelismo muito mais fácil. Atualmente, com o avanço dos processadores multi-core isso se torna bem importante, principalmente se você tem uma aplicação que deverá escalar para vários usuários e quer aproveitar o máximo o desempenho das suas máquinas. Com o pensamento funcional também ganhamos códigos mais legíveis, robustos e de fácil manutenção.

O que é programação funcional?

Programação funcional é um paradigma que trata a computação como uma avaliação de funções matemáticas evitando estado ou dados mutáveis. Aqui vão os principais conceitos:

1. Funções

Na programação funcional utilizamos apenas funções. Não há objetos, esqueça-os. Funções podem ser separadas em módulos de acordo com suas responsabilidades e semântica. Além disso, elas são “cidadãs de primeira classe”, podemos passa-las através de argumentos para outras funções e também utiliza-las em atribuições e declarações de tipo. Funções que recebem outras funções como argumento são chamadas de funções de alta ordem.

Um exemplo bem comum de função de alta ordem é o map. Comumente utilizada em linguagens funcionais, esta função recebe como argumentos uma expressão lambda de um parâmetro e uma lista :

Função map, exemplo em Haskell

O map itera sobre a lista, passando cada elemento da lista como argumento para a expressão lambda, a lógica então é processada, logo, o elemento antigo é substituido pelo retorno da expressão lambda.

Pode-se entender uma expressão lambda como sendo uma função anônima. Não nomeamos ela no corpo do nosso código, apenas reconhecemos o que ela faz, a expressamos dentro de um contexto específico, como no caso do map.

Em Ruby expressões lambdas também podem ser expressadas através de “blocos” e “procs”.

Função map em Ruby
Função map, output

Recaptulando, quando dizemos que utilizamos apenas funções, estamos usando o sentido literal.

No exemplo abaixo temos um código em Haskell (Linguagem de programação funcional pura). Este código pode ser interpretado errôneamente como uma atribuição de variável:

Na verdade, o que ocorre acima é a criação de uma função chamada x que retorna o valor 5, um equivalente em Ruby seria:

Talvez com esse exemplo você ja tenha “sacado” que na programação funcional pura não existe variáveis.

Sim, isso mesmo. Mas programando com uma linguagem dessas você irá descobrir que variáveis não são tão necessárias, incrivelmente. Como nos exemplos acima, fazemos algo bem parecido mas com semântica diferente.

Um software funcional é formado única e exclusivamente pela composição de funções. O que leva o nosso código a ser escrito de uma maneira declarariva, e não imperativa.

2. Estilo declarativo

Para entender o que é o “maneira” ou “estilo declarativo”, primeiro vamos entender o que quer dizer “estilo imperativo”.

Com “estilo imperativo” nos referenciamos a maneira de programar voltada a descrever como o software deve realizar cada tarefa, linha por linha, através de comandos. Essa definição parece meio estranha, porque programar até então, de acordo com os nossos conceitos, é isso. Mas observe os dois exemplos abaixo:

Programação imperativa
Programação não imperativa (declarativa)

Se formos ler os dois códigos em voz alta, entenderemos claramente o que queremos dizer com estilo imperativo!

Imperativo:

uma lista é igual a [1,2,3,4]!”

“a variável i é igual a [1,2,3,4]!”

“enquanto i for menor que o tamanho total da lista execute abaixo!”

“pegue o valor da lista na posição i e multiplique-o por dois !”

“atribua o valor de volta à lista, na mesma posição !”

“pegue o valor de i e adicione 1 !”

“atribua a soma a i !”

“mostre a lista na tela !”

Não imperativo (declarativo):

“Um mapeamento da lista [1,2,3,4] multiplicando cada um dos seus valores por dois e sendo mostrado na tela !”

A maneira declarativa de programar reduz drásticamente a quantidade de linhas que utilizamos em nosso software e aumenta drásticamente a legibilidade.

Nos dois exemplos, o nosso objetivo era realizar a mesma tarefa. A diferença é que no primeiro dizemos ao computador o passo a passo de como realiza-la, e no segundo apenas declaramos o que deve ser feito.

3. Imutabilidade

A Orientação a Objetos está diretamente ligada com estado. Em diversos casos, o seu objeto vai se comportar de maneira diferente de acordo com o estado em que sua aplicação se encontra. Para trabalhar com threads ou algum tipo de programação concorrente dentro desse contexto temos que nos preocupar bastante com a sincronização de variáveis e estado entre as linhas de execução, convenhamos que não é uma tarefa fácil, podendo levar a nossa lógica a complexidades extremas.

“Se em algum ponto os dados estiverem em um estado inconsistente (por não existir ou possuir um valor inesperado), o sistema todo pode estar em risco.”

— Brizeno M. (Refatorando com padrões de projeto — um guia em Ruby)

Algo com o qual não nos preocupamos utilizando programação funcional:

Na programação funcional “Nada muda, tudo se transforma.”

Bob Esponja

Bonito , não? pois é, aqui isso é mais do que filosofia, é lei. A partir do momento em que você declara que x é igual a 5, x vai ser igual a 5 para sempre. Poderíamos fazer uma associação as famosas constantes que estamos acostumados. No Ruby temos várias funções(métodos) que podem alterar os valores internos de uma variável, curiosamente eles costumam terminar com o “!”, justamente para tomarmos cuidado.

Caso de transformação:

“Nada muda, Tudo se transforma”, código
“Nada muda, Tudo se transforma”, output

No exemplo acima, o valor dentro de x foi transformado a partir do map e atribuido a y, que agora tem o valor [2,4,6,8], porém x continua sendo [1,2,3,4]. Em uma linguagem de programação funcional o valor de x nunca mudará, apenas se transformará.

Caso de mutação (O valor de x mudou):

Mutabilidade, código
Mutabilidade, output

O map com “!” altera o valor interno de x. Isso é totalmente inaceitável no paradígma funcional puro.

“A programação funcional é um paradigma de programação que trata apenas de aplicação de funções matemáticas, evitando a alteração de estado e mutabilidade de dados. Ou seja, assim que uma variável é alocada na memória e um valor é associado a este local, tal valor não pode ser mudado e sim transformado por uma aplicação de função.”

— Alexandre Garcia de Oliveira (Programador e matemático, autor do livro Haskell — uma introdução a programação funcional publicado pela casa do código)

4. Pureza

Na programação imperativa ou orientada a objetos é muito comum a presença de métodos(funções) que podem retornar diferentes resultados a partir dos mesmos parâmetros, isso é caracterizado como impureza.

O exemplo abaixo tem apenas o objetivo de ser lúdico. É claro que você não irá implementar uma função soma dois que pega o 2 de um valor externo(e ainda global). Mas dentro de outros contextos isso é algo que ocorre muitas vezes:

Impureza

Vamos supor que temos uma função soma_dois, que recebe um argumento inteiro y e soma mais dois, porém o dois está armazenado em um contexto externo. Se chamarmos soma_dois 5, esperamos que o retorno seja 7. Porém, temos outra função chamada muda_x que muda nosso contexto externo para 4. Se chamarmos muda_x e logo em seguida chamarmos soma_dois 5, teremos 9 como retorno! Então entende-se que a função soma_dois é impura, pois nos trouxe um resultado diferente mesmo sendo chamada com o mesmo argumento.

Esse tipo de condição, em softwares grandes com muitas regras de negócios também pode se tornar o motivo de bugs e complexidade desnecessária se não há algo bem documentado e testado.

Na programação funcional pura não existem efeitos colaterais. Todas as funções programadas devem ter o mesmo retorno a partir de um mesmo parâmetro. Isso é bom porque ganhamos em consistência.

A fundamentação matemática rigorosa da programação funcional nos permite escrever testes de software mais precisos e baseados em propriedades matemáticas que permitem escrever uma prova matemática de modo a validar um código.

— Alexandre Garcia de Oliveira (Programador e matemático, autor do livro Haskell - uma introdução a programação funcional publicado pela casa do código)

A partir do momento que temos certeza de como uma função deve se comportar e sabemos o que ela deverá retornar, também ganhamos uma maior facilidade na hora de escrever testes!

Since pure code has no dealings with the outside world, and the data it works with is never modified, the king od nasty surprise in which one piece of code invisibly corrupts data used by another is very rare. Whatever context we use a pure function in, the function will behavy consistently.

— Bryan O’sULLIVAN

Mesmo que na programação funcional a sua função use um valor externo, sabemos que esse valor é imutável. Logo, temos certeza de que a função não irá quebrar e se manterá pura.

Mas agora temos um X na questão:

Como garantimos essa imutabilidade e pureza se em um software comum lidamos com banco de dados e dados vindos do usuário?

Não existem variáveis, e não temos mutabilidade dentro do nosso código puro. Mas e o usuário? Ele não vai inserir o mesmo valor sempre. O mesmo ocorre com valores vindo do banco de dados, nossas funções terão que lidar com isso!

Existe uma maneira incrível de lidar com esses dados “impuros” mantendo a pureza e imutabilidade do nosso código funcional. Ainda pretendo escrever um artigo sobre o assunto, mas enquanto isso você pode descobrir mais através desses links:

5. Tipagem e Estruturas

Em linguagens funcionais não temos classes e nem objetos. Você pode estar se perguntando o que faremos quando tivermos que lidar com uma abstração de dados complexa. Para essas situações contamos com a tipagem nas linguagens funcionais fortemente tipadas (Haskell) ou estruturas em outros casos (Elixir, Erlang).

Uma das maiores belezas de se programar com uma linguagem de programação funcional pura e fortemente tipada, especificamente Haskell, é poder contar com o seu sistema de tipos totalmente baseado na matemática.

Teoria das Categorias e Haskell. Fonte http://yannesposito.com/Scratch/en/blog/Category-Theory-Presentation/

A imagem acima é um modelo baseado na Teoria das Categorias mostrando como alguns tipos de Haskell se encaixam de maneira matemática através de suas funções.

Diferente de outras abordagens, na funcional pura e fortemente tipada, temos uma segurança maior na hora de falarmos sobre robustez dos nossos aplicativos e testabilidade. Há a garantia da matemática, e o compilador agradece. Em geral, quando o seu código compila, é porque ele funciona, dificilmente teremos erro de código. Em linguagens como Haskell temos a vantagem de podermos utilizar tipos algébricos de dados, criar tipos e classes de tipos (typeclasses). Inicialmente, esses conceitos são difíceis de se absorver, pois começamos a abstrair a nossa lógica além do que estamos acostumados a ver na orientação a objetos e no “mundo normal”.

Uma associação que poderia ser feita entre os conceitos de tipagem e estruturas da programação funcional com a abstração da orientação a objetos é a seguinte:

Na orientação a objetos definimos uma classe carro, com atributos, e utilizamos esta classe dentro da lógica do nosso programa.

Definição de uma classe em Ruby

No Haskell, por exemplo, definimos um tipo carro, com um value constructor, e utilizamos este tipo dentro da lógica do nosso programa.

Definição de um tipo em Haskell
Resultado da função mostraCarro

Assim como podemos compor classes na Orientação a Objetos, podemos compôr tipos e estuturas na programação funcional, por exemplo:

Todo carro tem uma cor. Seria totalmente aceitável criarmos um tipo cor, e dizer que todo carro contêm um tipo cor.

Composição de tipos
Resultado, composição de tipos

Além de tudo, podemos criar tipos que portam um ou mais tipos variáveis dentro! Você conhece os generics do Java e C#? Se sim, temos ai algo bem parecido. Se não, imagine como se tivesse um porta malas no nosso carro, você pode colocar qualquer coisa dentro dele, sendo String, Int, lista e etc..

Tipos compostos por tipos variáveis
Resultado, tipos compostos por tipos variáveis

E se o nosso porta malas estiver vazio? Continuando nossa brincadeira, poderíamos criar um tipo “Talvez” que valida se o porta malas tem algo ou está vazio.

Tipo “Talvez” para validação
Resultado, tipo “talvez” para validação parte 1
Resultado, tipo “talvez” para validação parte 2

Utilizar a tipagem aqui é tão ou mais importante do que na orientação a objetos, pois precisamos garantir a integridade das nossas funções. Em Haskell, por exemplo, mesmo que você não declare o tipo da função o compilador automaticamente vai inferir um tipo para você.

Todos nós sabemos que Ruby, assim como Python, é uma linguagem de tipagem dinâmica e não estática. Isso significa que não definimos o tipo de uma variável. O tipo de uma variável será equivalente ao seu valor no momento.

Felizmente, para Ruby, temos uma biblioteca chamada dry-rb, esta biblioteca acrescenta tipagem para a linguagem com as gems ‘dry-type’ e ‘dry-struct’ veja mais em: http://dry-rb.org/gems/dry-types/

Utilizando o dry-rb conseguimos uma aproximação maior aos conceitos de tipagem e estruturas semelhantes a programação funcional, a biblioteca é bem abrangente e podemos fazer muitas coisas legais com ela, abaixo temos um exemplo bem simples:

No código acima, também criamos um “tipo” ou “estrutura” carro que funciona de maneira bem semelhante, se tentarmos por exemplo, atribuir uma cor que não foi definida no Dry::Struct “Cor” será lançado um erro de código. Além do mais, se tentarmos adicionar qualquer coisa que não seja uma estrutura Cor, também teremos erro de código. O mesmo vale para carro e qualquer uma que criarmos.

O dry-rb nos dá um leque de opções, sugiro que dê uma olhada em sua documentação.

6. Pensamento OO vs Pensamento FP

Mostrarei agora como o no nosso pensamento muda ao desenvolver um software da maneira funcional versus a maneira orientada a objetos.

Imagine que vamos criar um sistema simples para jogo de cartas, logo:

  • Teremos as cartas.
  • As cartas terão um valor e um naipe.
  • Realizaremos ações com um deck de cartas como: embaralhar, inserir cartas e remover cartas.

Na maneira orientada a objetos criaremos duas classes, Carta e Deck.

Na classe Deck teremos um atributo que portará cartas (uma lista de objetos do tipo Carta) e também as funções(métodos) embaralhar, adicionar_carta, e retirar_ultima_carta.

Os métodos do objeto Deck irão operar sobre os seus dados locais(atributos) de acordo com o seu comportamento dentro da nossa lógica.

Na orientação a objetos agrupamos dados e comportamento em classes e suas instâncias, visando sempre encapsular seus estados internos:

Abordagem orientada a objetos de deck e cartas

Já em uma abordagem funcional teremos o seguinte cenário:

Criaremos dois tipos(ou estruturas) Carta e Deck(este que nada mais é do que uma lista de Carta: Deck = Deck [Carta]) e um módulo Cartas onde teremos funções puras para lidarmos com os tipos criados.

E ai temos um dos “grandes gols” da programação funcional. Não juntamos dados e comportamentos em objetos.

Aqui dados e comportamento são coisas completamente separadas.

Na abordagem funcional criamos tipos (ou estruturas) e funções puras para lidar com eles:

Abordagem funcional de deck e cartas

Dessa forma, além de garantir o funcionamento do software de maneira pura e facilitar na hora de escrever testes (sem precisar ficar mockando objetos), evitamos bastante o surgimento de “efeitos colaterais” no nosso código, estes que geralmente podem ser causados pela mudança de comportamento em um determinado objeto a partir do momento em que seu dado interno também muda.

Lembrando que aqui os dados são imutáveis, a função embaralhar, por exemplo, não irá alterar o valor interno de um deck, ela irá retornar um novo deck com base no recebido como parâmetro.

7. Conceitos e Princípios funcionais utilizando Ruby

Ruby foi uma linguagem criada para agradar a nós, programadores. Com o intuito de ser simples e agradável, Ruby absorve vários conceitos que também estão presentes em linguagens funcionais, como, por exemplo, a omissão da palavra “return” no retorno das funções, a ausência de parênteses nas chamadas de funções, closures, currying, e alta ordem.

O seu criador,Yukihiro “Matz” Matsumoto, uniu partes das suas linguagens favoritas (Perl, Smalltalk, Eiffel, Ada e Lisp) para formar uma nova linguagem que equilibra a programação funcional com a programação imperativa. (Ruby)

https://www.ruby-lang.org/pt/about

Funções de alta ordem

Como explicado no começo do artigo, funções de alta ordem são funções que recebem outras funções como argumento. No Ruby , além do map, temos diversos outros exemplos bastante úteis para o nosso dia a dia, podemos destacar as funções:

Select

Com a função select, podemos “filtrar” uma lista, retornando apenas os elementos que satisfazem uma determinada condição. Recebe uma lista e uma função que retorna um booleano e retorna uma nova lista:

Reduce

Com a função reduce, podemos retornar um único valor a partir de uma lista e uma lógica entre os elementos da lista, por exemplo:

No primeiro exemplo acima estamos somando os elementos da lista, porém dizendo que devemos parar de somar caso o resultado já esteja maior do que 10. funciona assim:

x recebe o primeiro valor, y o segundo. O primeiro valor será sempre o resultado da última “iteração”.

Podemos passar um valor inicial para o reduce como:

Closures

Pode-se dizer que uma closure é uma função que pode armazenar valores dentro de um contexto, poderíamos associar como a definição de “atributos” em uma classe. Congelamos valores dentro de um escopo:

Quando atribuimos a chamada da função “contador” a variável “c”, o valor “acumula” ficar “preso” para sempre dentro de “c”. “c” herdará o comportamento da função anônima retornada. Por isso que cada vez que chamamos “c” temos um incremento no valor acumula.

Currying

Na programação funcional, currying, de maneira bem simplificada, significa pegarmos a lógica de uma função e a re-utilizarmos dentro de um contexto em que um ou mais dos seus parâmetros ja esteja pré-definido. Esta prática economiza linhas de código e facilita a sua reutilização.

No exemplo acima criamos duas funções genéricas, uma para operações e outra para mensagens de boas vindas. Utilizamos currying para herdarmos seus comportamentos em outras funções!

Abaixo temos um exemplo de currying e closure sendo usados simultaneamente:

Criamos uma segunda função baseada no currying de “conta_a_partir_de” . Dessa forma mantivemos dois valores “congelados” dentro do contexto da segunda função “conta_a_partir_de_5”: “acumula” e “numero”.

Obs:. Lambdas podem ser escritas em Ruby tanto com a sintaxe “->(){}” como com “lambda{}”. Nos exemplos foram usadas as duas formas.

8. Links Externos e Referências

Bem vindo ao paradigma funcional!

Trabalho atualmente com Ruby e Elixir. Tenho estudado bastante sobre programação funcional desde suas raízes matemáticas. O objetivo desse artigo é ajudar a disseminar conhecimento sobre esse incrível mundo e te proporcionar novos insights na hora de resolver problemas e criar soluções. Seja através de linguagens imperativas como Ruby ou funcionais como Haskell, Elixir e Scala.

Se gostou, segue eu!

Abraços!

;)

--

--