Spécifications du dialyseur: le chemin des Jedi

Il existe deux types de développeurs utilisant erlang et elixir: ceux qui écrivent des spécifications pour Dialyzer et ceux qui ne le sont pas encore. Au début, il semble que ce soit une perte de temps, en particulier pour ceux qui viennent de langues avec une frappe lâche. Cependant, ils m'ont aidé à détecter plus d'une erreur avant l'étape CI et - tôt ou tard - tout développeur comprend qu'ils sont nécessaires; non seulement comme un outil de guidage pour la saisie semi-stricte, mais aussi comme une grande aide dans la documentation du code.


Mais, comme c'est toujours le cas dans notre monde cruel, dans n'importe quel baril de bien, vous ne pouvez pas vous passer d'une cuillère. En substance, les directives @spec dupliquent le code de déclaration de fonction. Ci-dessous, je vais vous dire comment vingt lignes de code aideront à combiner la spécification et la déclaration d'une fonction dans une conception du formulaire


 defs is_forty_two(n: integer) :: boolean do n == 42 end 

Comme vous le savez, dans l'élixir, il n'y a que des macros. Même Kernel.defmacro/2 est une . Par conséquent, tout ce que nous devons faire est de définir notre propre macro qui, à partir de la construction ci-dessus, créera à la fois la déclaration de spécification et de fonction.


Eh bien, commençons.


Étape 1. Étudiez la situation.


Pour commencer, nous comprendrons quel type d'AST notre macro recevra comme arguments.


 defmodule CustomSpec do defmacro defs(args, do: block) do IO.inspect(args) :ok end end defmodule CustomSpec.Test do import CustomSpec defs is_forty_two(n: integer) :: boolean do n == 42 end end 

Ici, le formateur se rebellera , ajoutera des crochets et formatera le code à l'intérieur afin que les larmes coulent des yeux. Le sevrer de cela. Modifiez le .formatter.exs configuration .formatter.exs comme ceci:


 [ inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"], export: [locals_without_parens: [defs: 2]] ] 

Revenons à nos moutons et voyons ce que defs/2 y arrive. Il convient de noter que notre IO.inspect/2 fonctionnera au stade de la compilation (si vous ne comprenez pas pourquoi, vous n'avez pas encore besoin de jouer avec des macros, lisez le brillant livre Elixir de métaprogrammation de Chris McCord). Pour que le compilateur ne jure pas, nous retournons :ok (les macros doivent retourner le bon AST). Donc:


 {:"::", [line: 7], [ {:is_forty_two, [line: 7], [[n: {:integer, [line: 7], nil}]]}, {:boolean, [line: 7], nil} ]} 

Ouais. L'analyseur considère que la chose principale ici est l'opérateur :: , qui colle la définition de fonction et le type de retour. La définition de la fonction contient également une liste de paramètres sous forme de Keyword - Keyword , "nom de paramètre → type".


Étape 2. Échec rapide.


Comme nous avons jusqu'à présent décidé de ne prendre en charge qu'une telle syntaxe d'appel, nous devons réécrire la définition de la macro defs sorte que, par exemple, si le type de retour n'est pas spécifié, le compilateur jure immédiatement.


 defmacro defs({:"::", _, [{fun, _, [args_spec]}, {ret_spec, _, nil}]}, do: block) do 

Eh bien, il est temps de commencer la mise en œuvre.


Étape 3. Générez des spécifications et des déclarations de fonction.


 defmodule CustomSpec do defmacro defs({:"::", _, [{fun, _, [args_spec]}, {ret_spec, _, nil}]}, do: block) do #     args = for {arg, _spec} <- args_spec, do: Macro.var(arg, nil) #    args_spec = for {_arg, spec} <- args_spec, do: Macro.var(spec, nil) quote do @spec unquote(fun)(unquote_splicing(args_spec)) :: unquote(ret_spec) def unquote(fun)(unquote_splicing(args)) do unquote(block) end end end end 

Tout ici est tellement transparent qu'il n'y a même rien à commenter.


Il est temps de voir à quoi va aboutir l'appel à CustomSpec.Test.is_forty_two(42) :


 iex> CustomSpec.Test.is_forty_two 42 #⇒ true iex> CustomSpec.Test.is_forty_two 43 #⇒ false 

Et bien ça marche.


Étape 4. C'est tout?


Non, bien sûr. Dans la vie réelle, vous devrez gérer correctement les appels non valides, les définitions d'en-tête pour les fonctions avec plusieurs paramètres par défaut différents, collecter soigneusement la spécification, avec les noms de variables, vous assurer que tous les noms d'arguments sont différents, et bien plus encore. Mais comme une preuve de performance fera l'affaire.


En principe, vous pouvez toujours surprendre vos collègues avec quelque chose comme ceci:


 defmodule CustomSpec do defmacro __using__(_) do import Kernel, except: [def: 2] import CustomSpec defmacro def(args, do: block) do defs(args, do: block) end end ... end 

(Il reste encore defs/2 à corriger, générant Kernel.def au lieu de def ), mais je déconseille fortement de faire cela.


Merci pour votre attention, macro santé!

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


All Articles