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
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)
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
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)))
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)))))
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
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
Porém, se executamos a soma do 0 até 100 mil, temos um StackOverflow.
(soma (range 100000))
; Execution error (StackOverflowError)
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
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"
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"
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)
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
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")
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)))
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)