Existem dois tipos de desenvolvedores que usam erlang e elixir: aqueles que escrevem especificações para o Dialyzer e aqueles que ainda não o são . A princípio, parece que é tudo uma perda de tempo, especialmente para aqueles que vêm de idiomas com digitação simples. No entanto, eles me ajudaram a detectar mais de um erro antes do estágio do IC e - mais cedo ou mais tarde - qualquer desenvolvedor entende que é necessário; não apenas como uma ferramenta de orientação para digitação semi-estrita, mas também como uma grande ajuda na documentação de códigos.
Mas, como sempre acontece em nosso mundo cruel, em qualquer barril do bem, você não pode ficar sem uma colher. Em essência, as diretivas @spec
duplicam o código de declaração da função. Abaixo, mostrarei como vinte linhas de código ajudarão a combinar a especificação e a declaração de uma função em um design do formulário
defs is_forty_two(n: integer) :: boolean do n == 42 end
Como você sabe, no elixir não há nada além de macros. Até o Kernel.defmacro/2
é uma
. Portanto, tudo o que precisamos fazer é definir nossa própria macro, que a partir da construção acima criará declaração de especificação e função.
Bem, vamos começar.
Etapa 1. Estude a situação.
Para começar, entenderemos que tipo de AST nossa macro receberá 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
Aqui, o formatador irá se rebelar , adicionar colchetes e formatar o código dentro deles, para que as lágrimas fluam dos olhos. Desmame ele disso. Altere o .formatter.exs
configuração .formatter.exs
assim:
[ inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"], export: [locals_without_parens: [defs: 2]] ]
Vamos voltar às nossas ovelhas e ver o que defs/2
chega lá. Deve-se observar que nosso IO.inspect/2
funcionará no estágio de compilação (se você não entender o porquê, ainda não precisará usar macros, leia o brilhante livro Metaprogramming Elixir de Chris McCord). Para que o compilador não jure, retornamos :ok
(as macros devem retornar o AST correto). Então:
{:"::", [line: 7], [ {:is_forty_two, [line: 7], [[n: {:integer, [line: 7], nil}]]}, {:boolean, [line: 7], nil} ]}
Sim O analisador considera que o principal aqui é o operador ::
, que cola a definição da função e o tipo de retorno. A definição da função também contém uma lista de parâmetros na forma de Keyword
, “nome do parâmetro → tipo”.
Etapa 2. Falha rapidamente.
Como até agora decidimos dar suporte apenas a uma sintaxe de chamada, precisamos reescrever a definição da macro defs
para que, por exemplo, se o tipo de retorno não for especificado, o compilador jure imediatamente.
defmacro defs({:"::", _, [{fun, _, [args_spec]}, {ret_spec, _, nil}]}, do: block) do
Bem, é hora de começar a implementação.
Etapa 3. Gere especificações e declarações de função.
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
Tudo aqui é tão transparente que não há nada para comentar.
Chegou a hora de ver o que a chamada para CustomSpec.Test.is_forty_two(42)
levará a:
iex> CustomSpec.Test.is_forty_two 42 #⇒ true iex> CustomSpec.Test.is_forty_two 43 #⇒ false
Bem, isso funciona.
Etapa 4. Isso é tudo?
Não é claro. Na vida real, você terá que lidar corretamente com chamadas inválidas, definições de cabeçalho para funções com vários parâmetros padrão diferentes, coletar cuidadosamente as especificações, com nomes de variáveis, garantir que todos os nomes de argumentos sejam diferentes e muito mais. Mas como prova de desempenho servirá.
Em princípio, você ainda pode surpreender os colegas com algo assim:
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
(Ainda assim, o defs/2
precisará ser corrigido, gerando o Kernel.def
vez do def
), mas eu recomendo fortemente que não faça isso.
Obrigado por sua atenção, saúde macro!