La métaprogrammation est une technique à la fois puissante et complexe, qui permet à un programme de s’analyser et même de se modifier pendant l’exécution. De nombreuses langues modernes supportent cette fonctionnalité, et Elixir ne fait pas exception..
Avec la métaprogrammation, vous pouvez créer de nouvelles macros complexes, définir et différer de manière dynamique l'exécution du code, ce qui vous permet d'écrire du code plus concis et puissant. Il s’agit bien d’un sujet avancé, mais après la lecture de cet article, vous obtiendrez une compréhension de base sur la façon de commencer à utiliser la métaprogrammation dans Elixir..
Dans cet article, vous apprendrez:
citation
et fin de citation
les fonctions sont.Avant de commencer, cependant, laissez-moi vous donner un petit conseil. Rappelez-vous l'oncle de Spider Man a déclaré: «Un grand pouvoir implique une grande responsabilité»? Cela peut aussi s'appliquer à la métaprogrammation car il s'agit d'une fonctionnalité très puissante qui vous permet de tordre et de plier du code selon votre volonté..
Néanmoins, vous ne devez pas en abuser et vous devez vous en tenir à des solutions plus simples quand cela est sain et possible. Trop de métaprogrammation peut rendre votre code beaucoup plus difficile à comprendre et à maintenir, alors faites attention..
La première chose à comprendre est la manière dont notre code Elixir est représenté. Ces représentations sont souvent appelées arbres de syntaxe abstraite (AST), mais le guide officiel Elixir recommande de les appeler simplement expressions citées.
Il semble que les expressions se présentent sous la forme de nuplets à trois éléments. Mais comment pouvons-nous le prouver? Eh bien, il y a une fonction appelée citation
qui retourne une représentation pour un code donné. Fondamentalement, le code se transforme en un forme non évaluée. Par exemple:
quote do 1 + 2 end # => : +, [contexte: Elixir, importation: Noyau], [1, 2]
Alors qu'est-ce qui se passe ici? Le tuple rendu par le citation
function a toujours les trois éléments suivants:
:+
, ce qui signifie que nous effectuons l'addition. À propos, cette forme d’écriture devrait vous être familière si vous venez du monde Ruby..Noyau
le module a été importé automatiquement pour nous.1
et 2
.La représentation peut être beaucoup plus complexe, bien sûr:
quote do Enum.each ([1,2,3], & (IO.puts (& 1))) end # => :., [], [: __ aliases__, [alias: false], [: Enum ],: chaque], [], # [[1, 2, 3], # : &, [], # [:.,], [: __ aliases__, [alias: false], [: IO],: met], [], # [: &, [], [1]
D'autre part, certains littéraux se retournent quand ils sont cités, en particulier:
Dans l'exemple suivant, nous pouvons voir que citer un atome renvoie cet atome:
citation do: salut fin # =>: salut
Maintenant que nous savons comment le code est représenté sous le capot, passons à la section suivante et voyons ce que sont les macros et pourquoi les expressions citées sont importantes..
Les macros sont des formes spéciales, telles que des fonctions, mais celles qui renvoient du code cité. Ce code est ensuite placé dans l'application et son exécution est différée. Ce qui est intéressant, c'est que les macros n'évaluent pas non plus les paramètres qui leur sont transmis, elles sont également représentées par des expressions citées. Les macros peuvent être utilisées pour créer des fonctions personnalisées et complexes utilisées tout au long de votre projet..
Cependant, gardez à l’esprit que les macros sont plus complexes que les fonctions habituelles, et le guide officiel précise qu’elles ne devraient être utilisées qu’en dernier recours. En d'autres termes, si vous pouvez utiliser une fonction, ne créez pas de macro, car ainsi votre code devient inutilement complexe et, en réalité, plus difficile à maintenir. Néanmoins, les macros ont leurs cas d'utilisation, voyons comment en créer un.
Tout commence par le defmacro
call (qui est en fait une macro elle-même):
defmodule MyLib ne teste defmacro (arg) ne arg |> IO.inspect end end
Cette macro accepte simplement un argument et l'imprime.
En outre, il convient de mentionner que les macros peuvent être privées, tout comme les fonctions. Les macros privées ne peuvent être appelées qu'à partir du module où elles ont été définies. Pour définir une telle macro, utilisez defmacrop
.
Créons maintenant un module séparé qui servira de terrain de jeu:
defmodule Main nécessite MyLib def start! do MyLib.test (1,2,3) end end Main.start!
Quand vous exécutez ce code, : , [ligne: 11], [1, 2, 3]
sera imprimé, ce qui signifie en effet que l'argument a une forme entre guillemets (non évaluée). Avant de poursuivre, cependant, laissez-moi faire une petite note.
Pourquoi dans le monde avons-nous créé deux modules distincts: un pour définir une macro et un autre pour exécuter le code exemple? Il semble que nous devions procéder de cette façon, car les macros sont traitées avant l'exécution du programme. Nous devons également nous assurer que la macro définie est disponible dans le module, et cela à l'aide de exiger
. Cette fonction permet de s'assurer que le module donné est compilé avant le module actuel..
Vous pourriez vous demander, pourquoi ne pouvons-nous pas nous débarrasser du module principal? Essayons de faire ceci:
defmodule MyLib ne teste defmacro (arg) ne arg | | IO.inspect end end MyLib.test (1,2,3) # => ** (UndefinedFunctionError) fonction MyLib.test / 1 est indéfinie ou privée. Cependant, il existe une macro portant le même nom et la même arité. Assurez-vous de demander MyLib si vous souhaitez invoquer cette macro # MyLib.test (1, 2, 3) # (elixir) lib / code.ex: 376: Code.require_file / 2
Malheureusement, une erreur s'est produite indiquant que le test de fonction est introuvable, bien qu'il existe une macro portant le même nom. Cela se produit parce que le MyLib
module est défini dans la même portée (et le même fichier) que nous essayons de l’utiliser. Cela peut sembler un peu étrange, mais pour l’instant, rappelez-vous qu’un module distinct devrait être créé pour éviter de telles situations..
Notez également que les macros ne peuvent pas être utilisées globalement: vous devez d'abord importer ou demander le module correspondant..
Nous savons donc comment les expressions Elixir sont représentées en interne et quelles sont les macros… Et maintenant? Eh bien, maintenant nous pouvons utiliser cette connaissance et voir comment le code cité peut être évalué.
Revenons à nos macros. Il est important de savoir que le dernière expression de toute macro est censé être un code cité qui sera exécuté et retourné automatiquement lorsque la macro est appelée. Nous pouvons réécrire l'exemple de la section précédente en déplaçant IO.inspect
au Principale
module:
defmodule MyLib ne teste defmacro (arg) ne arg end end fin defmodule Main besoin de MyLib def start! do MyLib.test (1,2,3) |> IO.inspect end end Main.start! # => 1, 2, 3
Voir ce qui se passe? Le tuple renvoyé par la macro n'est pas cité mais évalué! Vous pouvez essayer d'ajouter deux entiers:
MyLib.test (1 + 2) |> IO.inspect # => 3
Une fois encore, le code a été exécuté et 3
a été retourné. Nous pouvons même essayer d'utiliser le citation
directement, et la dernière ligne sera toujours évaluée:
defmodule MyLib ne teste defmacro (arg) ne arg |> IO.inspect devis ne 1,2,3 fin end end #… def start! do MyLib.test (1 + 2) |> IO.inspect # => : +, [ligne: 14], [1, 2] # 1, 2, 3 end
le se disputer
a été cité (notez, en passant, que nous pouvons même voir le numéro de ligne où la macro a été appelée), mais l'expression citée avec le tuple 1,2,3
a été évalué pour nous car il s’agit de la dernière ligne de la macro.
Nous pouvons être tentés d’utiliser le se disputer
dans une expression mathématique:
defmacro test (arg) do quote do arg + 1 end end
Mais cela va générer une erreur en disant que se disputer
n'existe pas. Pourquoi Ceci est dû au fait se disputer
est littéralement inséré dans la chaîne que nous citons. Mais ce que nous aimerions plutôt faire, c’est évaluer la se disputer
, insérez le résultat dans la chaîne, puis effectuez la citation. Pour ce faire, nous aurons besoin d’une autre fonction appelée fin de citation
.
fin de citation
est une fonction qui injecte le résultat de l'évaluation du code dans le code qui sera ensuite cité. Cela peut sembler un peu bizarre, mais en réalité, les choses sont assez simples. Modifions l'exemple de code précédent:
defmacro test (arg) ne cite pas unquote (arg) + 1 end end
Maintenant notre programme va revenir 4
, ce qui est exactement ce que nous voulions! Qu'est-ce qui se passe est que le code transmis à la fin de citation
la fonction est exécutée uniquement lorsque le code cité est exécuté, et non lors de son analyse initiale.
Voyons un exemple un peu plus complexe. Supposons que nous voulions créer une fonction qui exécute une expression si la chaîne donnée est un palindrome. Nous pourrions écrire quelque chose comme ceci:
def if_palindrome_f? (str, expr) do si str == String.reverse (str), do: expr end
le _F
suffixe signifie ici qu’il s’agit d’une fonction, car nous créerons ultérieurement une macro similaire. Cependant, si nous essayons d’exécuter cette fonction maintenant, le texte sera imprimé même si la chaîne n’est pas un palindrome:
def start! do MyLib.if_palindrome_f? ("745", IO.puts ("oui")) # => "oui" fin
Les arguments passés à la fonction sont évalués avant que la fonction ne soit appelée, nous voyons donc "Oui"
chaîne imprimée à l'écran. Ce n’est en effet pas ce que nous voulons réaliser, alors essayons plutôt d’utiliser une macro:
defmacro if_palindrome? (str, expr) ne cite que si (unquote (str) == String.reverse (unquote (str))) ne pas unquote (expr) end end end ... MyLib.if_palindrome? ("745", IO. met ("oui"))
Ici, nous citons le code contenant le si
condition et utilisation fin de citation
inside pour évaluer les valeurs des arguments lors de l’appel de la macro. Dans cet exemple, rien ne sera imprimé à l'écran, ce qui est correct!
En utilisant fin de citation
n’est pas le seul moyen d’injecter du code dans un bloc cité. Nous pouvons également utiliser une fonctionnalité appelée contraignant. En fait, il s’agit simplement d’une option transmise au citation
fonction qui accepte une liste de mots-clés avec toutes les variables à ne pas citer juste une fois.
Pour effectuer la liaison, passez bind_quoted
au citation
fonctionne comme ceci:
quote bind_quoted: [expr: expr] do end
Cela peut s'avérer utile lorsque vous souhaitez que l'expression utilisée à plusieurs endroits ne soit évaluée qu'une seule fois. Comme le montre cet exemple, nous pouvons créer une macro simple qui génère une chaîne deux fois avec un délai de deux secondes:
defmodule MyLib teste defmacro (arg) cite bind_quoted: [arg: arg] ne arg | | IO.inspect Process.sleep 2000 arg |> IO.inspect fin fin fin
Maintenant, si vous l'appelez en passant l'heure système, les deux lignes auront le même résultat:
: os.system_time |> MyLib.test # => 1547457831862272 # => 1547457831862272
Ce n'est pas le cas avec fin de citation
, parce que l'argument sera évalué deux fois avec un léger retard, les résultats ne sont donc pas les mêmes:
defmacro test (arg) cite ne cite pas (arg) |> IO.inspect Process.sleep (2000) un devis (arg) |> IO.inspect end end #… def start! do: os.system_time |> MyLib.test # => 1547457934011392 # => 1547457936059392 end
Parfois, vous voudrez peut-être comprendre à quoi ressemble votre code cité pour le déboguer, par exemple. Cela peut être fait en utilisant le to_string
une fonction:
defmacro if_palindrome? (str, expr) do quoted = quote do (si (unquote (str) == String.reverse (unquote (str))) ne termine pas entre guillemets (expr) |> Macro.to_string |> IO.inspect cité fin
La chaîne imprimée sera:
"if (\" 745 \ "== String.reverse (\" 745 \ ")) do \ n IO.puts (\" yes \ ") \ nend"
Nous pouvons voir que le donné str
l'argument a été évalué et le résultat a été inséré directement dans le code. \ n
ici signifie "nouvelle ligne".
En outre, nous pouvons développer le code cité en utilisant expand_once
et développer
:
def start! do quoted = quote do MyLib.if_palindrome? ("745", IO.puts ("yes")) end quoted |> Macro.expand_once (__ ENV__) |> IO.inspect end
Qui produit:
: if, [contexte: MyLib, importation: Noyau], [: ==, [contexte: MyLib, importation: Noyau], ["745", :., [], [: __ aliases__, [alias : false, counter: -576460752303423103], [: String],: reverse], [], ["745"], [do: ::., [], [: __ aliases__, [alias : false, counter: -576460752303423103], [: IO],: met], [], ["oui"]]]
Bien sûr, cette représentation citée peut être retournée à une chaîne:
cité |> Macro.expand_once (__ ENV__) |> Macro.to_string |> IO.inspect
Nous obtiendrons le même résultat qu'auparavant:
"if (\" 745 \ "== String.reverse (\" 745 \ ")) do \ n IO.puts (\" yes \ ") \ nend"
le développer
La fonction est plus complexe car elle essaie de développer chaque macro dans un code donné:
cité |> Macro.expand (__ ENV__) |> Macro.to_string |> IO.inspect
Le résultat sera:
"case (\" 745 \ "== String.reverse (\" 745 \ ")) fait \ nx lorsque x dans [false, nil] -> \ nn \ n _ -> \ n IO.puts (\" oui \ ") \ nend"
Nous voyons cette sortie parce que si
est en fait une macro elle-même qui repose sur la Cas
déclaration, donc il est élargi aussi.
Dans ces exemples, __ENV__
est un formulaire spécial qui renvoie des informations sur l'environnement, telles que le module, le fichier, la ligne, la variable actuelle et les importations.
Vous avez peut-être entendu dire que les macros sont réellement hygiénique. Cela signifie qu'ils n'écrasent aucune variable en dehors de leur portée. Pour le prouver, ajoutons un exemple de variable, essayons de changer sa valeur à différents endroits, puis produisons-le:
defmacro if_palindrome? (str, expr) do autre_var = "if_palindrome?" quoted = quote do other_var = "quoted" if (unquote (str) == String.reverse (unquote (str))) ne pas citer (expr) end other_var |> IO.inspect end other_var |> IO.inspect quoted end final ... def start! do other_var = "start!" MyLib.if_palindrome? ("745", IO.puts ("oui")) other_var |> IO.inspect end
Alors other_var
a reçu une valeur à l'intérieur du début!
fonction, à l'intérieur de la macro et à l'intérieur du citation
. Vous verrez la sortie suivante:
"if_palindrome?" "cité" "commencer!"
Cela signifie que nos variables sont indépendantes et que nous n'introduisons aucun conflit en utilisant le même nom partout (bien que, bien sûr, il serait préférable de rester à l'écart d'une telle approche).
Si vous avez vraiment besoin de changer la variable externe depuis une macro, vous pouvez utiliser var!
comme ça:
defmacro if_palindrome? (str, expr) do quoted = quote do var! (other_var) = "quoted" if (unquote (str) == String.reverse (unquote (str))) fait unquote (expr) end end quoted end # … Def start! do other_var = "start!" MyLib.if_palindrome? ("745", IO.puts ("oui")) other_var |> IO.inspect # => "cité" fin
En utilisant var!
, nous disons effectivement que la variable donnée ne doit pas être hygiénisée. Soyez toutefois très prudent lorsque vous utilisez cette approche, car vous risquez de perdre le fil de ce qui est écrasé..
Dans cet article, nous avons abordé les bases de la métaprogrammation dans le langage Elixir. Nous avons couvert l'utilisation de citation
, fin de citation
, des macros et des liaisons tout en voyant des exemples et des cas d'utilisation. À ce stade, vous êtes prêt à appliquer ces connaissances à la pratique et à créer des programmes plus concis et plus puissants. Cependant, rappelez-vous qu'il est généralement préférable d'avoir un code compréhensible qu'un code concis, évitez donc de trop utiliser la métaprogrammation dans vos projets..
Si vous souhaitez en savoir plus sur les fonctionnalités que j'ai décrites, n'hésitez pas à lire le guide de démarrage officiel sur les macros, les guillemets et les guillemets. J'espère vraiment que cet article vous a donné une bonne introduction à la métaprogrammation dans Elixir, ce qui peut sembler assez complexe au début. En tout cas, n’ayez pas peur d’expérimenter avec ces nouveaux outils!
Je vous remercie de rester avec moi et à bientôt.