Elixir Métaprogramming Basics

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:

  • Qu'est-ce que l'arbre de syntaxe abstrait et comment le code Elixir est représenté sous le capot.
  • Ce que le citation et fin de citation les fonctions sont.
  • Que sont les macros et comment les utiliser?.
  • Comment injecter des valeurs avec la liaison.
  • Pourquoi les macros sont hygiéniques.

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

Arbre de syntaxe abstraite et Citation

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:

  1. Atome ou un autre tuple avec la même représentation. Dans ce cas, c'est un atome :+, ce qui signifie que nous effectuons l'addition. À propos, cette forme d’écriture devrait vous être familière si vous venez du monde Ruby..
  2. Liste de mots clés avec métadonnées. Dans cet exemple, nous voyons que le Noyau le module a été importé automatiquement pour nous.
  3. Liste des arguments ou un atome. Dans ce cas, il s'agit d'une liste avec les arguments 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:

  • des atomes
  • entiers
  • des flotteurs
  • des listes
  • des cordes
  • tuples (mais seulement avec deux éléments!)

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

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.

Exiger

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

Macros et expressions entre guillemets

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.

Sans citer le code

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!

Injecter des valeurs avec des liaisons

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

Conversion du code cité

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.

Les macros sont hygiéniques

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é..

Conclusion

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.