DEV Community

Laura Fumagalli
Laura Fumagalli

Posted on

Programação funcional - primeiras impressões com Clojure

O intuito deste artigo é ser a primeira parte de um material introdutório à Programação Funcional. Usarei Clojure, uma linguagem que usa esse paradigma muito diferente da tradicional Orientação a Objetos amplamente utilizada no mercado de trabalho e ensinada em faculdades tradicionais. Se você tem a mente aberta para uma nova linguagem de programação e acredita que uma sintaxe e paradigma novos te levarão a novos horizontes, este artigo é para você! Vou também fazer algumas comparações com linguagens historicamente consideradas orientadas a objeto (como Java, por exemplo).

Logo de início, já vou trazer que a Programação Funcional tem muito a agregar. Com ela, é possível escrever códigos mais robustos, menos suscetíveis a erros e expandir sua forma de pensar. Por exemplo, a imutabilidade, um conceito comum nesse paradigma, minimiza a possibilidade de encontrarmos defeitos oriundos da manipulação de estado em lugares desconhecidos. Uma grande vantagem de aprender Programação Funcional é utilizar os benefícios do paralelismo, que trarei aqui mais adiante. Escrever código paralalizável em uma linguagem de paradigma funcional é muito mais fácil. A ausência de efeitos colaterais nas funções de um programa permite que essas funções sejam executadas sem uma ordem definida e em paralelo. A programação funcional nos ajuda a pensar sempre em construir funções sem efeitos colaterais.

Por que Clojure?

Essa linguagem possui recursos interessantes para nos ajudar a manter o foco nos aspectos inerentes à linguagem e à Programação ƒuncional: a sintaxe, que é simples e muito diferente das linguagens mais populares, e um sistema dinâmico de tipos, o que vai nos permitir evitar pensar em Orientação a Objetos por um tempo. Clojure é um dialeto Lisp. Enquanto todas as outras linguagens, como Java, Golang, C# etc, derivam do Algol (como C), linguagens de Paradigma Funcional, como Clojure, Scala ou Elixir por exemplo, são da família do Lisp. Lisp é uma linguagem que trouxe a ideia de que é possível utilizar apenas funções matemáticas para reprezentar estruturas de dados básicas, aliado ao conceito de código como dado. Depois do "choque inicial", é possível perceber que a sintaxe é extremamente simples, com poucos símbolos ou palavras-chaves reservadas.

Clojure roda na máquina virtual Java, o que permite que programas escritos nesta linguagem usem bibliotecas escritas em Java! Dito isto, você precisa instalar o kit de desenvolvimento do Java, JDK, antes de instalar o Clojure em si. Basta seguir o passo a passo da própria documentação no site oficial deles.

Primeiro código em Clojure (e o REPL)

Algo que pode parecer estranho no começo é o fato de não depurar o código da forma tradicional em Clojure. REPL é um ambiente imterativo onde escrevemos códigos e eles são interpretados de imediato, gerando resultados muito rápidos. É ideal para trechos pequenos e validação de ideias.

REPL é um acrônimo para read, evaluate, print e loop. Isto quer dizer que ele vai ler nossos comandos, interpretá-los, exibir na tela o resultado e repetir o processo.

Programar no REPL é algo bem comum entre quem tem familiaridade com Clojure. Claro, em algum momento, nossos códigos vão parar em arquivos e serão empacotados. Mas o REPL fornece um ambiente muito prático para experimentação e testes. Algumas pessoas substituem desenvolvimento guiado por testes pelo constante uso do REPL, mas eu não recomendo. É sempre melhor criar casos de testes para garantir a consistência e qualidade do código, independente da linguagem. Se hoje você sente dificuldade com TDD, ou Desenvolvimento Orientado por Testes (Test-Driven Development), aprender a desenvolver em Clojure é uma excelente oportunidade de treinar essa prática.

Para entender esse tal de REPL na prática, vamos usar o Boot "clj". Basta abrir o terminar e digitar clj ou clojure que vai abrir um terminal interativo esperando você fornecer alguma informação, o terminal diz: user=>. Isso significa que já podemos escrever código! É possível sair desse contexto apertando "ctrl + d".

Primeiros contatos com Clojure

Aqui vou fazer uma abordagem um pouco diferente sobre essa linguagem. O código em Clojure me lembrou muito RPN (Reverse Polish Notation / Notação Polonesa Reversa). RPN basicamente é um método para escrever expressões matemáticas onde os operadores (+, -, *) são colocados depois dos números (operandos), eliminando a necessidade de parênteses. Exemplo de uso: "O motor de cálculo converte a fórmula (10 + 5) * 2 para a expressão RPN 10 5 + 2 * para poder processá-la". É normalmente usado em sistemas de back-end, como calculadoras e motores de regras (rule engines), para avaliar de forma rápida e sem ambiguidade fórmulas complexas de juros, tarifas ou limites de crédito.

Entrando no REPL e digitando (+ 1 2) o resultado, como esperado, é 3. Mas a sintaxe parece bem diferente das linguagens com as quais estamos acostumadas. Vamos à explicação:

Acontece que a linha dentro do () é uma lista composta de +, 1 e 2. O primeiro elemento dessa lista é sempre uma função que é executada e os demais elementos desta lista são argumentos para esta função. O mesmo se aplica para as outras operações aritméticas:

(* 2 3)
(/ 2 2)
(- 0 2)
Enter fullscreen mode Exit fullscreen mode

Se for uma operação um pouco mais complexa, fica da seguinte forma:

(* 2 (+ 3 3))
;; isto é um comentário em Clojure e vamos
;; utilizá-lo para demonstrar o retorno das funções
;; 12
Enter fullscreen mode Exit fullscreen mode

Este exemplo é onde Clojure mostra mais um diferencial no sentido de sintaxe, fruto de sua herança de Lisp. Pensando na matemática, é claro que a soma será executada primeiro. O diferencial, porém, é que a linguagem exige os parênteses, o que não deixa margem alguma para dúvidas do que precede o quê. A ordem de execução de código é sempre de dentro para fora.

E para concatenar Strings? Assim: (str "Hello, " "world!")
Podemos também verificar se duas Strings são iguais:

(= "Hello" "Hi")`
;; false
(= "Hi" "Hi")
;; true
Enter fullscreen mode Exit fullscreen mode

Importante observar que, diferente de muitas linguagens, o = é uma função em Clojure que verifica se duas coisas são iguais. O = não é um operador de associação, normalmente utilizado na construção de variáveis.

E como verificamos se um número é par?

(even? 2)
;; true
Enter fullscreen mode Exit fullscreen mode

E se um número é múltiplo de 3?

(= 0 (mod 9 3))
;; true
Enter fullscreen mode Exit fullscreen mode

Neste último exemplo, o que acontece é que verificamos o módulo da divisão entre 9 e 3. O resultado será utilizado como o segundo argumento na função que verifica igualdade, =.

Nossas próprias funções

Começando do básico, vamos criar uma função que recebe um nome e da um "oi". Para criar funções, fazemos o seguinte:

(defn oi [nome]
  (str "Oi, " nome "!"))
Enter fullscreen mode Exit fullscreen mode

Namespaces

Ao criar a função é possível ler a saída: #'user/oi
Isto quer dizer que alguma coisa com o nome oi acabou de ser criada, e encontra-se no namespace padrão, user. Namespaces em Clojure representam a mesma ideia que em outras linguagens, como pacotes em Java, sendo uma forma de agrupar funções. A combinação do namespace e do nome da função forma o identificador de tal função

A função +, por exemplo, é encontrada no namespace clojure.core, sendo o identificador clojure.core/+.
Como o namespace clojure.core é disponibilizado por padrão, a função + está sempre disponível.
Funções em outros namespaces precisam ser incluídas no nosso cófigo antes de serem utilizadas.

Com a função criada, vamos invocar com o nome desejado:

(oi "zé")
;; "Oi, zé!"
Enter fullscreen mode Exit fullscreen mode
  1. O defn nos indica que vamos criar uma função.
  2. Depois, damos um nome a ela (neste caso, oi).
  3. Logo a seguir, vem a lista de argumentos, cercada por [ e ]. Neste caso, temos apenas um argumento, então fica [nome].
  4. Em seguida vem o que realmente é executado: a concatenação de Strings. Note que não precisamos definir o que será retornado. A última instrução é o que será retornado.

O Trecho que concatena Strings tem uma particularidade: é aplicado 3 argumentos. Esta é uma de várias funções que são aplicáveis em uma quantidade indeterminada de argumentos.

E como seria uma função que verifica se um número é múltiplo de 3?

(defn multiplo-de-3? [dividendo]
  (= 0 (mod dividendo 3)))
Enter fullscreen mode Exit fullscreen mode

Então podemos chamar a função assim:

(multiplo-de-3? 9)
;; true

(multiplo-de-3? 20)
;; false
Enter fullscreen mode Exit fullscreen mode

Sobre os condicionais

Para simplificar o exemplo, vamos exibir a palavra sim se um número for par, e, caso contrário, a palavra não. O código fica assim:

(defn par? [numero]
  (if (even? numero)
    "sim"
    "não"))
Enter fullscreen mode Exit fullscreen mode

Dessa forma, temos a saída:

(par? 5)
;; "não"
(par? 4)
;; "sim"
Enter fullscreen mode Exit fullscreen mode

if tem muito cara de uma função também, mas ela é na verdade o que é chamado de forma especial: um recurso base do Clojure.

O if recebe 3 argumentos: O primeiro é uma verificação que retorna verdadeiro ou falso. Os demais argumentos representam algo a ser executado de acordo com o resultado da verificação. Em caso de verdadeiro, o segundo argumento é avaliado e retornado. Caso contrário, o argumento 3 é que é avaliado e retornado.

É importante salientar que apenas nil e false são considerados realmente falsos para verificação de condições. Outros, como 0 e String vazia, que são comuns em outras linguagens, serão avaliados como verdadeiros!

Consultando a documentação

Sempre que você quiser saber mais sobre algum recurso do Clojure, você pode experimentar a documentação dentro do REPL!
Para isso, use a função doc:
(doc if)

Agora, quando houver outra condição além de duas, a macro cond pode ajudar. Ela recebe pares de condicionais e expressões. Segue um exemplo:

(defn saldo [valor]
  (cond (< valor 0) "negativo"
    (> valor 0) "positivo"
    :else "zero"))
Enter fullscreen mode Exit fullscreen mode

Quando testamos:

(saldo 900)
;; "positivo"

(saldo -900)
;; "negativo"

(saldo 0)
;; "zero"
Enter fullscreen mode Exit fullscreen mode

Neste exemplo, quando o valor é menor que zero, o primeiro teste, logo de cara, retorna verdadeiro, e a expressão que o segue, "negativo" , é avaliada (neste caso, não faz nada, apenas retorna a String "negativo"). Quando o valor é maior que zero, o primeiro teste ((< valor 0)) falha, retornando falso, mas o teste seguinte retorna verdadeiro, e a expressão correspondente é avaliada. Agora, quando o valor é exatamente 0, as duas primeiras verificações retornam falso, e a última verificação (:else) é validada e "zero" é retornado.

O que significa este :else? Nada demais, na real. Como qualquer coisa diferente de nil e false é considerada verdadeira, qualquer coisa que colocássemos ali no lugar do :else funcionaria como uma forma de fazer com que "zero" fosse retornado por padrão. Se quiser, teste lá com valores como 1 ou "milhão". Bem, não é realmente qualquer coisa: números (mesmo que 0), Strings (mesmo que vazias), caracteres, coleções... O :else é apenas uma convenção adotada por algumas pessoas as quais eu andei copiando.

Conclusão

Ufa! Acho que agora temos a capacidade de aumentar um pouco mais a complexidade dos problemas que podemos tratar. Aqui aprendemos como interagir com o REPL do Clojure, chamar funções, criar nossas próprias funções e trabalhar com condicionais. No próximo artigo, resolveremos um problema bastante comum no mundo da programação: o Fizz-Buzz. Você verá como a solução para este problema fica bem sucinta em Clojure.

Top comments (0)