Dialyzer specs: путь джедая

от автора

Есть два типа разработчиков, использующих эрланг и эликсир: те, кто пишет спеки для Dialyzer, и те, кто пока нет. Поначалу кажется, что это все пустая трата времени, особенно тем, кто пришел из языков с нестрогой типизацией. Однако они помогли мне отловить не одну ошибку еще до стадии CI, и — рано или поздно — любой разработчик понимает, что они нужны; не только как инструмент наведения полустрогой типизации, но и как отличное подспорье в документировании кода.

Но, как это всегда бывает в нашем жестоком мире, в любой бочке пользы — без ложки не обойтись. По сути, директивы @spec дублируют код объявления функции. Ниже я расскажу, как двадцать строчек кода помогут объединить спецификацию и объявление функции в конструкцию вида

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

Как известно, в эликсире нет ничего, кроме макросов. Даже Kernel.defmacro/2 — это макрос. Поэтому все, что нам потребуется — определить собственный макрос, который из конструкции выше создаст и спеку, и объявление функции.

Ну что ж, приступим.

Шаг 1. Изучение ситуации.

Начнем с того, что поймем, что за AST наш макрос получит в качестве аргументов.

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

Здесь взбунтуется formatter, понарасставит скобок и отформатирует код внутри них так, что из глаз потекут слезы. Отучим его от этого. Изменим файл конфигурации .formatter.exs вот таким образом:

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

Вернемся к нашим баранам и посмотрим, что там получает на вход defs/2. Надо заметить, что наш IO.inspect/2 отработает на стадии компиляции (если вы не понимаете, почему, не нужно пока играться с макросами, лучге почитать блистательную книжку «Metaprogramming Elixir» Криса Маккорда). Чтобы компилятор не заругался, мы возвращаем :ok (макросы обязаны возвращать корректный AST). Итак:

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

Угу. Парсер считает, что главный тут — оператор ::, склеивающий определение функции и возвращаемый тип. Определение функции также содержит список параметров в виде Keyword, «имя параметра → тип».

Шаг 2. Fail fast.

Поскольку мы пока решили поддерживать только такой синтаксис вызова, нужно переписать определение макроса defs таким образом, чтобы если, например, возвращаемый тип не указан — компилятор ругался сразу.

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

Ну что ж, пора и к реализации приступать.

Шаг 3. Генерация спеки и объявления функции.

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

Здесь все настолько прозрачно, что даже и комментировать нечего.

Пора посмотреть, к чему приведет вызов CustomSpec.Test.is_forty_two(42):

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

Ну что ж, оно работает.

Шаг 4. И все?

Да нет, конечно. В реальной жизни придется правильно обработать неверные вызовы, заголовочные определения для функций с несколькими различными параметрами по умолчанию, аккуратнее собрать спеку, с именами переменных, удостовериться, что все имена аргументов различны, и много чего еще. Но в качестве доказательства работоспособности сойдет.

В принципе, можно еще удивить коллег при помощи чего-нибудь вот такого:

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

(Там еще defs/2 надо будет подправить, генерируя Kernel.def вместо def), но вот этого я бы делать настоятельно не рекомендовал.

Спасибо за внимание, макросите на здоровье!


ссылка на оригинал статьи https://habr.com/ru/post/480466/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *