Ganchos de tempo de compilação Elixir

O Elixir está equipado com uma infraestrutura macro sofisticada e muito bem projetada. Com a mão leve de Chris McCord, há uma lei não escrita na comunidade que é inevitavelmente dita imediatamente quando se trata de macros: "A primeira regra para o uso de macros é que você não deve usá-las". Às vezes, com uma observação discreta digitada em uma fonte cinza pálida do quarto tamanho: "somente se você não puder evitá-la e entender muito bem o que está indo e o que está arriscando". Isso se deve ao fato de as macros terem acesso a todo o AST do módulo em que são usadas e, de um modo geral, podem alterar o código resultante além do reconhecimento.


Em princípio, concordo que você não deve usar macros no processo de familiarização com o idioma. Até agora, você não pode, sendo acordado às três da manhã com uma ressaca, responder à pergunta se esse código é executado no estágio de compilação ou em tempo de execução. O Elixir é uma linguagem compilada e, durante o processo de compilação, o código de "nível superior" é executado, a árvore de sintaxe é totalmente expandida até nos encontrarmos em uma situação em que não há mais nada a abrir, e esse resultado é finalmente compilado no BEAM . Quando o compilador encontra uma chamada de macro no código-fonte, expõe completamente o AST e empurra em vez da chamada real . É impossível entender, só pode ser lembrado.


Porém, assim que nos sentirmos livres na sintaxe, inevitavelmente desejaremos usar todo o poder do kit de ferramentas; aqui sem macros - em lugar nenhum. O principal é não abusar. As macros podem reduzir drasticamente (para valores negativos) a quantidade de código padrão necessário, e fornecem uma maneira natural e muito conveniente de manipular o AST . Phoenix , Ecto e todas as bibliotecas notáveis ​​usam muito as macros.


O acima é verdadeiro para qualquer biblioteca / pacote universal. Na minha experiência, os projetos comuns provavelmente não são macros necessárias ou são necessários em uma área de aplicação muito limitada. As bibliotecas, por outro lado, geralmente consistem em macros na proporção de 80/20 em relação ao código normal.


Não vou arrumar uma caixa de areia aqui e esculpir bolinhos de macros para aqueles que ainda não estão muito conscientes do que comem; se for interessante começar a aprender com o básico, entender o que é em princípio ou como e por que eles são usados ​​no Elixir , é melhor fechar imediatamente esta página e ler o brilhante livro Metaprogramming Elixir de Chris McCord, criador do Phoenix Framework . Eu só quero demonstrar alguns truques para melhorar um macroecossistema existente.




As macros são intencionalmente mal documentadas. Esta faca é muito afiada para anunciar para crianças.


Existem duas maneiras de usar macros. O mais simples é que você instrua o compilador que este módulo usará macros de outro usando a Kernel.SpecialForms.require/2 e chame a própria macro depois disso (para macros definidas no mesmo módulo, não require necessário um require explícito). Neste artigo, estamos interessados ​​em outra maneira, mais complexa. Quando as chamadas de código externas use MyLib , espera-se que nosso módulo MyLib implemente a __using__/1 , que o compilador use MyLib quando encontrar o use MyLib . Açúcar sintático, sim. Convenção sobre configuração. O passado ferroviário de José não passou sem deixar rasto.


Atenção: se o parágrafo acima é intrigante para você, e tudo isso parece ridículo, pare de comer esse cacto e leia o livro que mencionei acima, em vez de minha nota de shortbread.


__using__/1 usa um argumento, para que o proprietário da biblioteca permita que os usuários passem alguns parâmetros para ele. Aqui está um exemplo de um dos meus projetos internos que usa uma chamada de macro com parâmetros:


 defmodule User do use MyApp.ActiveRecord, repo: MyApp.Repo, roles: ~w|supervisor client subscriber|, preload: ~w|setting companies|a 

Um argumento do tipo keyword() será passado para MyApp.ActiveRecord.__using__/1 , e lá eu o uso para criar vários auxiliares para trabalhar com este modelo. ( Nota: este código há muito tempo está bêbado porque o ActiveRecord perde em todos os aspectos as chamadas Ecto nativas).




Às vezes, queremos limitar o uso de macros a um subconjunto de módulos (por exemplo, permita que ele seja usado apenas em estruturas). Uma verificação explícita dentro da __using__/1 não funcionará, como gostaríamos, porque durante a compilação do módulo não temos acesso ao seu __ENV__ (e seria - ele estava longe de ser concluído no momento em que o compilador encontrou uma chamada __using__/1 Seria ideal realizar essa verificação após a compilação estar concluída.


Não tem problema! Existem dois atributos de módulo que configuram exatamente isso. Bem-vindo ao nos visitar, queridos retornos de chamada em tempo de compilação .


Aqui está um breve trecho da documentação.


@after_compile retorno de chamada será chamado imediatamente após a compilação do módulo atual.

Aceita um módulo ou tupla {module, function_name} . O retorno de chamada em si deve receber dois argumentos: o ambiente do módulo e seu bytecode. Quando apenas um módulo é passado como argumento, supõe-se que este módulo exporte a função __after_compile__/2 .

Os retornos de chamada registrados primeiro serão executados por último.
 defmodule MyModule do @after_compile __MODULE__ def __after_compile__(env, _bytecode) do IO.inspect env end end 

Eu não recomendo injetar __after_compile__/2 diretamente no código gerado, pois isso pode levar a conflitos com as intenções dos usuários finais (que podem querer usar seus próprios manipuladores). Defina uma função em algum lugar dentro do MyLib.Helpers ou algo assim e passe a tupla para @after_compile :


 quote location: :keep do @after_compile({MyLib.Helpers, :after_mymodule_callback}) end 



Esse retorno de chamada será chamado imediatamente após a compilação do módulo correspondente, que usa nossa biblioteca, e receberá dois parâmetros: a estrutura __ENV__ e o bytecode do módulo compilado. Este último é raramente usado por meros mortais; o primeiro fornece tudo o que precisamos. A seguir, é apresentado um exemplo de como me protejo de tentar chamar o use Iteraptable de módulos que não implementam estruturas. De fato, o código de verificação simplesmente chama do __struct__ de __struct__ __struct__ no módulo compilado e brega delega ao Elixir o direito de lançar uma exceção com texto não criptografado, explicando a causa do problema:


 def struct_checker(env, _bytecode), do: env.module.__struct__ 

O código acima lançará uma exceção se o módulo compilado não for uma estrutura. Obviamente, o código de verificação pode ser muito mais complicado, mas a idéia principal é se o seu módulo usado espera algo do módulo que o utiliza . @after_compile caso, faz sentido não esquecer @after_compile e xingar a partir daí, se todas as condições necessárias não forem atendidas. Lançar uma exceção é a abordagem correta, neste caso, um pouco mais do que sempre, pois esse código é executado no estágio de compilação.




Uma história engraçada é conectada ao @after_callback , o que explica completamente por que eu amo o OSS em geral e o Elixir em particular. Há cerca de um ano, cometi um erro ao copiar e colar e copiei de algum lugar @after_callback vez de @before_callback . A diferença entre eles é provavelmente óbvia: o segundo é chamado antes da compilação e a partir daí todos podem mudar a árvore da sintaxe além do reconhecimento. E eu - ah, como - eu mudei. Mas isso não levou a nenhum resultado no código compilado: não foi alterado. Depois de três xícaras de café, notei um erro de digitação, substituído after por before e tudo começou; mas a pergunta me atormentou: por que o compilador ficou calado, como um partidário? Aconteceu que Module.open?/1 retorna true partir deste retorno de chamada (que, em princípio, não está longe da verdade - o módulo ainda não está fechado, o acesso a seus atributos não está fechado e muitas bibliotecas usam esse bug não documentado).


Bem, esbocei uma correção, enviei uma solicitação pull para a crosta do idioma (para o compilador, se absolutamente estritamente) e, menos de um dia depois, ele acabou no mestre .


Foi então que eu precisei das configurações do usuário no IO.inspect/2 e, em alguns casos. O que aconteceria se eu topasse com isso em Java? - É assustador imaginar.




Tenha uma ótima macro!

Source: https://habr.com/ru/post/pt485810/


All Articles