Especificaciones del dializador: el camino del Jedi

Hay dos tipos de desarrolladores que usan erlang y elixir: los que escriben especificaciones para Dialyzer y los que aún no lo hacen. Al principio parece que todo es una pérdida de tiempo, especialmente para aquellos que provienen de idiomas con mecanografía suelta. Sin embargo, me ayudaron a detectar más de un error antes de la etapa de CI y, tarde o temprano, cualquier desarrollador comprende que son necesarios; no solo como una herramienta de guía para la escritura semi-estricta, sino también como una gran ayuda para documentar el código.


Pero, como siempre es el caso en nuestro mundo cruel, en cualquier barril del bien, no puedes prescindir de una cuchara. En esencia, las directivas @spec duplican el código de declaración de función. A continuación, le diré cómo veinte líneas de código ayudarán a combinar la especificación y la declaración de una función en un diseño del formulario.


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

Como saben, en el elixir no hay nada más que macros. Incluso Kernel.defmacro/2 es una . Por lo tanto, todo lo que tenemos que hacer es definir nuestra propia macro, que a partir de la construcción anterior creará tanto la especificación como la declaración de función.


Bueno, empecemos.


Paso 1. Estudia la situación.


Para empezar, entenderemos qué tipo de AST recibirá nuestra macro como argumentos.


 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 

Aquí el formateador se rebelará , agregará corchetes y formateará el código dentro de ellos para que las lágrimas fluyan de los ojos. Retíralo de esto. Cambie el .formatter.exs configuración .formatter.exs esta manera:


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

Volvamos a nuestras ovejas y veamos qué defs/2 llega allí. Cabe señalar que nuestro IO.inspect/2 funcionará en la etapa de compilación (si no comprende por qué, aún no necesita jugar con macros, lea el brillante libro Elixir de metaprogramación de Chris McCord). Para que el compilador no jure, devolvemos :ok (las macros deben devolver el AST correcto). Entonces


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

Si El analizador considera que lo principal aquí es el operador :: , que pega la definición de la función y el tipo de retorno. La definición de la función también contiene una lista de parámetros en forma de Keyword , "nombre del parámetro → tipo".


Paso 2. Falla rápido.


Como hasta ahora hemos decidido admitir solo esa sintaxis de llamada, necesitamos reescribir la definición de la macro defs para que, por ejemplo, si no se especifica el tipo de retorno, el compilador jure de inmediato.


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

Bueno, es hora de comenzar la implementación.


Paso 3. Generar especificaciones y declaraciones de funciones.


 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 

Todo aquí es tan transparente que no hay nada que comentar.


Es hora de ver lo que la llamada a CustomSpec.Test.is_forty_two(42) conducirá a:


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

Pues funciona.


Paso 4. ¿Eso es todo?


No por supuesto. En la vida real, tendrá que manejar correctamente llamadas inválidas, definiciones de encabezado para funciones con varios parámetros predeterminados diferentes, recopilar cuidadosamente la especificación, con nombres de variables, asegurarse de que todos los nombres de argumentos sean diferentes y mucho más. Pero como prueba de rendimiento lo hará.


En principio, todavía puedes sorprender a tus colegas con algo como esto:


 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 

(Aún habrá que corregir defs/2 , generando Kernel.def lugar de def ), pero recomendaría enfáticamente no hacerlo.


¡Gracias por su atención, macro salud!

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


All Articles