Есть два типа разработчиков, использующих эрланг и эликсир: те, кто пишет спеки для 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
), но вот этого я бы делать настоятельно не рекомендовал.
Спасибо за внимание, макросите на здоровье!