Como funcionam as macros em Clojure

Cesar Augusto Alcancio de Souza
8 min readAug 23, 2021

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.

No final do texto vamos entender cada código dessa macro.

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.

Função when-fn

Quando executamos a macro when e a função when-fn, validando se um número é positivo, vamos ter o mesmo comportamento:

A função funciona como a macro!

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?

A função não funciona como a macro!

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 stringpositive”.

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

--

--

Cesar Augusto Alcancio de Souza

Sofware Engineer Lead, focused on development and maintenance of products