Como funcionam as macros em Clojure
Se você já escreveu algum código em Clojure provavelmente você já se deparou com as famosas macros, que é definida utilizando a função defmacro e muitas vezes possui caracteres diferentes como apóstrofos (‘), acento grave (`), til (~), sinal tironiano ou e comercial (&), arroba (@) e cerquilha (#). Nesse texto você vai entender, de forma bem simples e direta, o que é uma macro e o que significa esses caracteres estranhos.
Segundo o site oficial do Clojure, uma macro é um sistema que permite estender o compilador com código Clojure, e cita o exemplo do when, que é uma macro composta pelas formas especiais if e do.
Por que não uma função em vez de uma macro?
Mas o que é estender o compilador exatamente e porque o when é uma macro e não uma função? Vamos tentar criar uma função que faça o mesmo que o when. Essa função vai se chamar when-fn.
Quando executamos a macro when e a função when-fn, validando se um número é positivo, vamos ter o mesmo comportamento:
Reparem que a macro e a função têm o mesmo retorno para os dois cenários, positivo e negativo, mas o que acontece se enviamos um bloco de código que deve ser executado apenas se o número é positivo ou apenas se o número é negativo?
Diferente da macro when, na função when-fn o println é sempre executado independente se o número é positivo ou negativo! Ou seja, não é possível estender o código criando a função when-fn, precisamos da defmacro. Isso porque as funções recebem o resultado de outras funções executadas enquanto a macro é capaz de receber os argumentos como lista, manipulá-los e executá-los ou deixar de executá-los (você vai entender mais adiante) além disso, as macros não retornam diretamente o resultado da operação, elas retornam listas (ou expandem para listas) porque a lista é como o Clojure reconhece suas funções, que vão ser posteriormente executadas.
Como funcionam as macros?
Macros recebem argumentos como lista e permitem que essa lista seja manipulada
Macros retornam listas manipuladas, esse é o processo de expansão, e posteriormente as listas serão executadas como uma função
Por exemplo, vamos supor que queremos criar nosso próprio when-print que escreve “Hello” antes de executar o bloco de código desejado. Nossa macro deve retornar uma lista parecida com o código abaixo que será posteriormente executado.
No lugar de <condicao> e <bloco-de-codigo> deve ser o código que foi enviado no momento em que macro foi invocada. Para isso podemos construir a macro when-print da seguinte maneira.
Utilizando essa macro com a mesma condicional do número positivo é possível ver o “Hello!” escrito na tela antes de retornar a string “positivo”.
Antes de nos aprofundarmos no código da macro, utilizando a função macroexpand é possível analisar a lista que será retornada pela nossa macro antes da sua execução.
A macro retorna uma lista como planejamos, onde a primeira posição é um if, a segunda posição é uma lista com a condicional que foi enviada, a terceira posição é uma nova lista que contém: do, uma lista com o println e uma string “positive”.
Para entender melhor como a macro retornou esse resultado, precisamos entender o que significa o apóstrofo (‘) antes dos símbolos if, do e println na declaração da macro.
Símbolo, valores e quote
O apóstrofo (‘) também conhecido como quote é a mesma coisa que utilizar a própria função quote em Clojure, ou seja, essa função permite que quando o símbolo seja executado retorne o próprio símbolo em vez de retornar seu valor, por exemplo:
Aqui definimos um símbolo com o valor de uma string “Cesar”, esse símbolo quando executado retorna o valor “Cesar”, porém se queremos retornar o próprio símbolo nome podemos utilizar a função quote ou simplesmente adicionar um apóstrofo antes da palavra para deixar o código menos verboso.
Dessa forma fica mais fácil entender como o código a seguir.
(list ‘if test (list ‘do (list ‘println “Hello!”) body))
Se transforma neste código.
(if (pos? 1) (do (println “Hello!”) “positive”))
Como isso funciona? A função list permite que os parênteses não sejam executado como uma função, e que em vez disso seja retornada uma lista.
Os símbolos if, do e println não são “avaliados” no momento em que a macro é expandida, os símbolos test e body são “avaliados” de acordo com seu valor, se transformando em (pos? 1) e “positive” respectivamente.
Também podemos notar que a função list retorna apenas os parênteses, ou seja, aparentemente é o mesmo que utilizar a função quote ou simplesmente o um apóstrofo, por exemplo:
Porque não utilizamos apóstrofo em vez da função list? A resposta é porque o quote mantém todos os valores da lista como símbolo e não é possível acessar o seu valor como fazemos para o test e body no exemplo, veja:
Nesse caso test e body não se transformaram em (pos? 1) e “positive”. E é aí que entra o acento grave (`) e o til (~).
Acento grave e til
Se utilizamos o acento grave no lugar do apóstrofo e combinamos ele com o til antes dos símbolos que queremos extrair o respectivo valor, pode ser escrita dessa forma:
Além de permitir esse comportamento o acento grave também utiliza o namespace completo das funções enquanto o apóstrofo apenas o nome da função:
Arroba e sinal tironiano ou e comercial
Mas onde entra o o arroba (@) nesse monte de caracteres? Para isso vamos utilizar o sinal tironiano (&) que, usado no vetor de argumentos, agrupa todos os últimos argumentos em uma lista, por exemplo:
Se aplicamos esse conceito para a nossa macro, ela ficaria dessa forma:
Repare que a macro expandida retorna uma lista de println dentro da função do, isso vai falhar porque o ((println “it’s”) (println “positive”)) vai ser transformado em (nil nil), que quando for executado irá gerar o famoso NullPointerException.
O correto seria extrair os dois println da lista e deixá-los diretamente dentro da função do, transformando ((println “it’s”) (println “positive”)) em (println “it’s) (println “positive”).
Para isso podemos utilizar o arroba antes do símbolo body, ficando ~@body, o til para extrair o valor e o arroba para removê-los de dentro da lista.
Cerquilha
Agora o comando que precisamos entender dentro de uma macro é o cerquilha (#). Vamos supor que queremos mudar um pouco nossa macro para usar um let para armazenar a string “Hello”.
Se tentamos executar a macro ou expandir a macro vamos receber um erro:
Isso acontece porque a macro tenta prevenir que o desenvolvedor utilize símbolos que já foram definidos antes e força o desenvolvedor a definir símbolos aleatórios com a função gensym, vamos testar ela.
Apenas com essa função a macro pode assegurar que o símbolo que está sendo criado na macro é aleatório e não foi definido antes. Atualizando a nossa macro com o gensym ficaria:
Nesse caso, tivemos que adicionar um let fora da nossa lista de retorno (fora do acento grave), que vai ser executado no momento que a macro for expandida. Vai criar o nosso símbolo “hello-str20026” e utilizá-lo posteriormente. E é aí que a cerquilha ajuda, podemos evitar a utilização da função gensym dentro da macro e substituir pela cerquilha nos símbolos em que queremos que sejam gerados aleatoriamente:
Dessa forma o símbolo generated-symbol é gerado automaticamente e podemos aproveitá-lo posteriormente na macro.
Conclusão
- Os retornos das funções não são enviadas a macro como em uma função normal, em vez disso são enviados como listas
- As macros podem manipular essa lista e retornar uma nova lista que será executada como uma função posteriormente
- A expansão da macro é o processo que transforma o código da macro em uma lista para ser executada posteriormente
- O apóstrofo (‘) é o mesmo que a função quote, que permite retornar o símbolo em vez do seu valor ou permite a criação de listas se usado antes dos parênteses
- O acento grave (`) funciona como o apóstrofo porém permite ser utilizado em combinação com o til (~) para os casos que queremos retornar o valor e não o símbolo
- O sinal tironiano (&) permite que uma função ou macro receba todos os últimos argumentos dentro de uma lista
- O arroba (@) dentro de uma macro permite extrair o conteúdo de uma lista, por exemplo, transformar (:a :b :c) em :a :b :c
- O cerquilha (#) dentro de uma macro permite criar símbolos aleatoriamente