DEV Community

Guilherme Rodrigues de Melo
Guilherme Rodrigues de Melo

Posted on

Programação Funcional em Clojure

Olá! Nesse post falaremos um pouco sobre programação funcional em Clojure. Bora? :)

Imutabilidade

Imutabilidade é útil quando desejamos evitar a mudança de estado em nosso sistema, principalmente quando temos que trabalhar com concorrência, pois caso haja alguma falha, torna-se difícil de debugar o mesmo para encontrar a verdadeira causa. Em Clojure, toda vez que manipulamos uma estrutura de dados, retornamos não a estrutura manipulada, mas sim uma nova versão da estrutura com as mudanças realizadas. Clojure utiliza de certas estruturas de dados internamente para que essa copia não tenha uma performance ruim.

Transparência Referencial

Transparência Referencial é quando podemos substituir uma expressão pelo seu valor sem mudar o comportamento do programa, ou seja, é quando definimos uma função que recebe um argumento e sempre retorna o mesmo resultado quando dado o mesmo argumento.

Exemplo de uma função referencialmente transparente:

(defn multiplicacao [x y]
    (* x y))

(multiplicacao 10 5)
; => 50
Enter fullscreen mode Exit fullscreen mode

No exemplo acima, sempre que passarmos os mesmos argumentos, o resultado será o mesmo.

Funções Puras

Funções puras são aqueles que atendem a dois requisitos. O primeiro deles é que a função deve retornar sempre o mesmo resultado dado os mesmos argumentos. O segundo requisito é que a função não deve causar nenhum efeito colateral, ou seja, ela não deve fazer nenhuma mudança fora da função em si, como por exemplo, mudando o valor de uma variável externa. Se a função alterar algum valor dentro dela ou algo afetar o resultado da função, então essa função não é pura. Exemplo:

(def numeros '(1 2 3 4 5))

(defn incrementa-numeros []
    (map inc numeros))

(incrementa-numeros)
; => (2 3 4 5 6)
Enter fullscreen mode Exit fullscreen mode

No exemplo acima, a função incrementa-numeros não recebe parâmetros, porém ela utiliza uma variável externa que poderia ser modificada antes dela ser invocada, podendo não retornar os mesmos valores dados os mesmos argumentos. No exemplo abaixo podemos ver uma versão pura da função anterior, onde a função recebe a sequência como parâmetro.

(def numeros '(1 2 3 4 5))

(defn incrementa-numeros [sequencia]
   (map inc sequencia))

(incrementa-numeros numeros)
; => 2 3 4 5 6
Enter fullscreen mode Exit fullscreen mode

Se uma função lê um arquivo, ela não é referencialmente transparente porque o conteúdo do arquivo pode se modificar. Nos exemplos abaixo existem as funções conta-caracteres e analisa-arquivo, sendo respectivamente, uma pura e outra não.

(defn conta-caracteres [texto]
    (str "Quantidade de caracteres:" (count texto)))

(defn analisa-arquivo [arquivo]
    (conta-caracteres (slurp arquivo)))
Enter fullscreen mode Exit fullscreen mode

Funções puras tornam a manutenção e a leitura do sistema mais clara, pois as funções ficam isoladas, sem impactar as demais funções e valores do sistema. Além de serem consistentes, pois utilizam do conceito de transparência referencial.

Trabalhando com Estrutura de Dados imutáveis

Todo programa deve ter funções impuras, porém essas funções devem ser em menor quantidade e bem isoladas. Clojure nos ajuda provendo estruturas de dados imutáveis em seu core que veremos a seguir.

Recursão em vez de for/while

Diferente de outras linguagens que utilizam de efeitos colaterais em loops como for e while, Clojure nos proporciona a alterativa para a mutação através da recursão. O exemplo abaixo demonstra a soma dos valores de um vetor através de recursão:

(defn soma 
    ([numeros] (soma numeros 0))
    ([numeros total]
        (if (empty? numeros)
            total
            (soma (rest numeros) (+ (first numeros) total)))))
Enter fullscreen mode Exit fullscreen mode

No exemplo acima é verificado se o vetor passado como parâmetro é vazio. Caso seja vazio o total da soma é retornado. No entanto, se o vetor ainda não é vazio, chama-se a função novamente passando como parâmetros o restante dos valores do vetor e a soma do primeiro item do vetor com o total acumulado até o momento. A função rest sempre devolve todos os itens do vetor, exceto o primeiro. Abaixo podemos ver como ocorrem as chamadas recursivas:

(soma [1 2 3 4 5])
(soma [1 2 3 4 5] 0)
(soma [2 3 4 5] 1)
(soma [3 4 5] 3)
(soma [4 5] 6)
(soma [5] 10)
(soma [] 15)
; => 15
Enter fullscreen mode Exit fullscreen mode

A cada chamada recursiva, um novo escopo é criado onde numeros e total são associados a diferentes valores, sem a necessidade de alterar os valores originais. Se executamos essa função para somar apenas do 0 até o 1000, tudo funciona corretamente.

(soma (range 1000))
; => 499500
Enter fullscreen mode Exit fullscreen mode

Porém, se executamos a soma do 0 até 100 mil, temos um StackOverflow.

(soma (range 100000))
; Execution error (StackOverflowError) 
Enter fullscreen mode Exit fullscreen mode

Por razões de performance e para evitar problemas desse tipo, Clojure recomenda a utilização da função recur se você estiver processando recursivamente uma coleção com milhares ou milhões de valores. Exemplo da função anterior utilizando recur.

(defn soma 
 ([numeros] (soma numeros 0))
 ([numeros total]
     (if (empty? numeros)
         total
         (recur (rest numeros) (+ (first numeros) total)))))

(soma (range 100000))
; => 4999950000
Enter fullscreen mode Exit fullscreen mode

Composição de função

Composição de função é o ato de combinar funções passando valores de uma função para outra. Ao utilizar composição de funções, passamos a ter um código mais reutilizável. Clojure oferece algumas funções que ajudam o desenvolvedor como a função comp.

Comp

A função comp tem o objetivo de criar uma nova função através de outras funções. Abaixo um exemplo simples de utilização:

((comp clojure.string/capitalize clojure.string/lower-case clojure.string/reverse) "GUILHERME")
; => "Emrehliug"
Enter fullscreen mode Exit fullscreen mode

As funções passadas como parâmetro para comp são executadas da direita para a esquerda, ou seja, sendo a ordem de execução: reverse, lower-case e capitalize. O código anterior é uma versão mais concisa do código abaixo:

(clojure.string/capitalize 
    (clojure.string/lower-case 
        (clojure.string/reverse "GUILHERME")))
; => "Emrehliug"
Enter fullscreen mode Exit fullscreen mode

Devemos usar comp para deixar nosso código mais fácil de entender e mais reutilizável. No exemplo a seguir vemos como a utilização de comp deixa tudo mais claro.

Utilizando comp:

(map (comp keyword str) ["Brasil" "França"])
; => (:Brasil :França)
Sem utilizar comp:

(map #(keyword (str %)) ["Brasil" "França"])
; => (:Brasil :França)
Enter fullscreen mode Exit fullscreen mode

Nos dois casos o resultado é o mesmo, porém no primeiro exemplo fica mais claro como as coisas acontecem.

Aplicação parcial com partial

A função partial recebe uma função e vários argumentos. Com isso, partial retorna uma nova função que, ao ser invocada, retorna a função original passada como parâmetro utilizando os parâmetros originais. Exemplo:

(def adiciona-cem (partial + 100))

(adiciona-cem 200)
; => 300
Enter fullscreen mode Exit fullscreen mode

No exemplo, quando chamamos adiciona-cem, ela chama a função + passando o valores 100 e 200 como parâmetro.

A função partial é útil quando desejamos reutilizar uma determinada combinação de funções e argumentos. No exemplo abaixo utilizamos partial para reaproveitar a geração do log:

(defn log 
    [nivel mensagem]
    (condp = nivel
        :erro (clojure.string/upper-case mensagem)
        :sucesso (clojure.string/lower-case mensagem)))

(def mensagem-erro (partial log :erro))

(def mensagem-sucesso (partial log :sucesso))

(mensagem-erro "Erro ao tentar acessar recurso")

(mensagem-sucesso "Recurso salvo")
Enter fullscreen mode Exit fullscreen mode

Memoize

Memoization nos da a vantagem da transparência referencial que citei no início do post, pois memoize guarda os parâmetros e o retorno da função. Dessa forma, quando houver várias chamadas para a mesma função com os mesmos argumentos, o resultado é retornado imediatamente. Em casos de funções que levam muito tempo para serem executadas, a função memoize é muito útil.

No exemplo abaixo temos uma primeira versão da função exibe que exibe a mensagem após 1 segundo. Na segunda versão da função utilizamos memoize para retornar o valor imediatamente após a primeira chamada.

(defn exibe [mensagem]
    (Thread/sleep 1000)
    mensagem)

(def exibe-com-memoize (memoize (defn exibe [mensagem]
    (Thread/sleep 1000)
    mensagem)))
Enter fullscreen mode Exit fullscreen mode

Espero que esse post tenha te ajudado a entender um pouco mais sobre Clojure e como usamos programação funcional nessa linguagem. Nos vemos num post futuro. :)

Top comments (0)