[Open source, Erlang/OTP, Elixir/Phoenix] Типы, где их не ждали
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Давайте представим себе реализацию модуля Scaffold, который генерирует структуру с предопределенными пользовательскими полями и инжектит ее в вызываемый модуль при помощи use Scaffold. При вызове use Scaffold, fields: foo: [custom_type()], ... — мы хотим реализовать правильный тип в Consumer модуле (common_field в примере ниже определен в Scaffold или еще где-нибудь извне).
@type t :: %Consumer{
common_field: [atom()],
foo: [custom_type()],
...
}
Было бы круто, если бы мы могли точно сгенерировать тип Consumer.t() для дальнейшего использования и создать соответствующую документацию для пользователей нашего нового модуля.
Пример посложнее будет выглядеть так:
defmodule Scaffold do
defmacro __using__(opts) do
quote do
@fields unquote(opts[:fields])
@type t :: %__MODULE__{
version: atom()
# magic
}
defstruct @fields
end
end
end
defmodule Consumer do
use Scaffold, fields: [foo: integer(), bar: binary()]
end
и, после компиляции:
defmodule Consumer do
@type t :: %Consumer{
version: atom(),
foo: integer(),
bar: binary()
}
defstruct ~w|version foo bar|a
end
Выглядит несложно, да?
Наивный подход
Давайте начнем с анализа того, что за AST мы получим в Scaffold.__using__/1.
defmacro __using__(opts) do
IO.inspect(opts)
end
#⇒ [fields: [foo: {:integer, [line: 2], []},
# bar: {:binary, [line: 2], []}]]
Отлично. Выглядит так, как будто мы в шаге от успеха.
quote do
custom_types = unquote(opts[:fields])
...
end
#⇒ == Compilation error in file lib/consumer.ex ==
# ** (CompileError) lib/consumer.ex:2: undefined function integer/0
Бамс! Типы — это чего-то особенного, как говорят в районе Привоза; мы не можем просто взять и достать их из AST где попало. Может быть, unquote по месту сработает?
@type t :: %__MODULE__{
unquote_splicing([{:version, atom()} | opts[:fields]])
}
#⇒ == Compilation error in file lib/scaffold.ex ==
# ** (CompileError) lib/scaffold.ex:11: undefined function atom/0
Как бы не так. Типы — это утомительно; спросите любого, кто зарабатывает на жизнь хаскелем (и это еще в хаскеле типы курильщика; настоящие — зависимые — типы в сто раз полезнее, но еще в двести раз сложнее).
Ладно, кажется, нам нужно собрать все это богатство в AST и инжектнуть его целиком, а не по частям, чтобы компилятор увидел сразу правильное объявление.
Построение типа в AST
Я опущу тут пересказ нескольких часов моих метаний, мучений, и тычков пальцем в небо. Все знают, что я пишу код в основном наугад, ожидая, что вдруг какая-нибудь комбинация этих строк скомпилируется и заработает. В общем, сложности тут с контекстом. Мы должны пропихнуть полученные определения полей в неизменном виде напрямую в макрос, объявляющий тип, ни разу не попытавшись это AST анквотнуть (потому что в момент unquote типы наподобие binary() будут немедленно приняты за обыкновенную функцию и убиты из базуки вызваны компилятором напрямую, приводя к CompileError.
Кроме того, мы не можем использовать обычные функции внутри quote do, потому что все содержимое блока, переданного в quote, уже само по себе — AST.
quote do
Enum.map([:foo, :bar], & &1)
end
#⇒ {
# {:., [], [{:__aliases__, [alias: false], [:Enum]}, :map]}, [],
# [[:foo, :bar], {:&, [], [{:&, [], [1]}]}]}
Видите? Вместо вызова функции, мы получили ее препарированное AST, все эти Enum, :map, и прочий маловнятный мусор. Иными словами, нам придется создать AST определения типа вне блока quote и потом просто анквотнуть внутри него. Давайте попробуем.
Чуть менее наивная попытка
Итак, нам надо инжектнуть AST как AST, не пытаясь его анквотнуть. Звучит устрашающе? — Вовсе нет, отнюдь.
defmacro __using__(opts) do
fields = opts[:fields]
keys = Keyword.keys(fields)
type = ???
quote location: :keep do
@type t :: unquote(type)
defstruct unquote(keys)
end
end
Все, что нам нужно сделать сейчас, — это произвести надлежащий AST, все остальное в порядке. Ну, пусть ruby сделает это за нас!
iex|1 quote do
...|1 %Foo{version: atom(), foo: binary()}
...|1 end
#⇒ {:%, [],
# [
# {:__aliases__, [alias: false], [:Foo]},
# {:%{}, [], [version: {:atom, [], []}, foo: {:binary, [], []}]}
# ]}
А нельзя ли попроще?
iex|2 quote do
...|2 %{__struct__: Foo, version: atom(), foo: binary()}
...|2 end
#⇒ {:%{}, [],
# [
# __struct__: {:__aliases__, [alias: false], [:Foo]},
# version: {:atom, [], []},
# foo: {:binary, [], []}
# ]}
Ну, по крайней мере, выглядит это не слишком отталкивающе и достаточно многообещающе. Пора переходить к написанию рабочего кода.
Почти работающее решение
defmacro __using__(opts) do
fields = opts[:fields]
keys = Keyword.keys(fields)
type =
{:%{}, [],
[
{:__struct__, {:__MODULE__, [], ruby}},
{:version, {:atom, [], []}}
| fields
]}
quote location: :keep do
@type t :: unquote(type)
defstruct unquote(keys)
end
end
или, если нет цели пробросить типы из собственно Scaffold, даже проще (как мне вот тут подсказали: Qqwy here). Осторожно, оно не будет работать с проброшенными типами, version: atom() за пределами блока quote выбросит исключение.
defmacro __using__(opts) do
fields = opts[:fields]
keys = Keyword.keys(fields)
fields_with_struct_name = [__struct__: __CALLER__.module] ++ fields
quote location: :keep do
@type t :: %{unquote_splicing(fields_with_struct)}
defstruct unquote(keys)
end
end
Вот что получится в результате генерации документации для целевого модуля (mix docs):
Примечание: трюк с фрагментом AST
Но что, если у нас уже есть сложный блок AST внутри нашего __using__/1 макроса, который использует значения в кавычках? Переписать тонну кода, чтобы в результате запутаться в бесконечной череде вызовов unquote изнутри quote? Это просто даже не всегда возможно, если мы хотим иметь доступ ко всему, что объявлено внутри целевого модуля. На наше счастье, существует способ попроще.
NB для краткости я покажу простое решение для объявления всех пользовательских полей, имеющих тип atom(), которое тривиально расширяеься до принятия любых типов из входных параметров, включая внешние, такие как GenServer.on_start() и ему подобные. Эту часть я оставлю для энтузиастов в виде домашнего задания.
Итак, нам надо сгенерировать тип внутри блока quote do, потому что мы не можем передавать туда-сюда atom() (оно взовется с CompileError, как я показал выше). Хначит, что-нибудь типа такого:
keys = Keyword.keys(fields)
type =
{:%{}, [],
[
{:__struct__, {:__MODULE__, [], ruby}},
{:version, {:atom, [], []}}
| Enum.zip(keys, Stream.cycle([{:atom, [], []}]))
]}
Это все хорошо, но как теперь добавить этот АСТ в декларацию @type? На помощь приходит очень удобная функция эликсира под названием Quoted Fragment, специально добавленный в язык ради генерации кода во время компиляциию Например:
defmodule Squares do
Enum.each(1..42, fn i ->
def unquote(:"squared_#{i}")(),
do: unquote(i) * unquote(i)
end)
end
Squares.squared_5
#⇒ 25
Quoted Fragments автоматически распознаются компилятором внутри блоков quote, с напрямую переданным контекстом (bind_quoted:). Проще простого.
defmacro __using__(opts) do
keys = Keyword.keys(opts[:fields])
quote location: :keep, bind_quoted: [keys: keys] do
type =
{:%{}, [],
[
{:__struct__, {:__MODULE__, [], ruby}},
{:version, {:atom, [], []}}
| Enum.zip(keys, Stream.cycle([{:atom, [], []}]))
]}
# ⇓⇓⇓⇓⇓⇓⇓⇓⇓⇓⇓⇓⇓
@type t :: unquote(type)
defstruct keys
end
end
Одинокий вызов unquote/1 тут разрешен, потому что bind_quoted: был напрямую указан как первый аргумент в вызове quote/2.
Удачного внедрения!
===========
Источник:
habr.com
===========
Похожие новости:
- [Open source, Git, Системы управления версиями, DevOps] Вышел релиз GitLab 13.5 с обновлениями для безопасности мобильных приложений и вики-страницами групп
- [Open source, PHP, Программирование, Компиляторы] ВКонтакте снова выкладывает KPHP
- [Open source, Терминология IT, IT-компании] Google вводит обязательную инклюзивную терминологию во всех своих открытых проектах
- [Open source, Облачные сервисы, Финансы в IT, Звук] Обсуждение: почему индустрия подкастов все больше походит на стриминг сериалов и фильмов
- [Open source, C++] Сборка Colobot Gold
- [Информационная безопасность, Open source] Maltego Часть 7. DarkNet matter
- [Open source, Разработка под iOS, Разработка под Linux, IT-компании] Apple запрещает приложения эмулятора терминала на iPhone: в текущих версиях через них можно скачивать код
- [Open source, Работа с видео, GitHub, Копирайт] «Фонд электронных рубежей» расценил удаление youtube-dl как злоупотребление DMCA
- [Open source, Отладка, Angular, Визуализация данных, Rust] Обновления в смотрелке логов
- [Open source, *nix] FOSS News №41 – дайджест новостей и других материалов о свободном и открытом ПО за 2-8 ноября 2020 года
Теги для поиска: #_open_source, #_erlang/otp, #_elixir/phoenix, #_macros, #_macro, #_injection, #_open_source, #_erlang/otp, #_elixir/phoenix
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:03
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Давайте представим себе реализацию модуля Scaffold, который генерирует структуру с предопределенными пользовательскими полями и инжектит ее в вызываемый модуль при помощи use Scaffold. При вызове use Scaffold, fields: foo: [custom_type()], ... — мы хотим реализовать правильный тип в Consumer модуле (common_field в примере ниже определен в Scaffold или еще где-нибудь извне). @type t :: %Consumer{
common_field: [atom()], foo: [custom_type()], ... } Было бы круто, если бы мы могли точно сгенерировать тип Consumer.t() для дальнейшего использования и создать соответствующую документацию для пользователей нашего нового модуля. Пример посложнее будет выглядеть так: defmodule Scaffold do
defmacro __using__(opts) do quote do @fields unquote(opts[:fields]) @type t :: %__MODULE__{ version: atom() # magic } defstruct @fields end end end defmodule Consumer do use Scaffold, fields: [foo: integer(), bar: binary()] end и, после компиляции: defmodule Consumer do
@type t :: %Consumer{ version: atom(), foo: integer(), bar: binary() } defstruct ~w|version foo bar|a end Выглядит несложно, да? Наивный подход Давайте начнем с анализа того, что за AST мы получим в Scaffold.__using__/1. defmacro __using__(opts) do
IO.inspect(opts) end #⇒ [fields: [foo: {:integer, [line: 2], []}, # bar: {:binary, [line: 2], []}]] Отлично. Выглядит так, как будто мы в шаге от успеха. quote do
custom_types = unquote(opts[:fields]) ... end #⇒ == Compilation error in file lib/consumer.ex == # ** (CompileError) lib/consumer.ex:2: undefined function integer/0 Бамс! Типы — это чего-то особенного, как говорят в районе Привоза; мы не можем просто взять и достать их из AST где попало. Может быть, unquote по месту сработает? @type t :: %__MODULE__{
unquote_splicing([{:version, atom()} | opts[:fields]]) } #⇒ == Compilation error in file lib/scaffold.ex == # ** (CompileError) lib/scaffold.ex:11: undefined function atom/0 Как бы не так. Типы — это утомительно; спросите любого, кто зарабатывает на жизнь хаскелем (и это еще в хаскеле типы курильщика; настоящие — зависимые — типы в сто раз полезнее, но еще в двести раз сложнее). Ладно, кажется, нам нужно собрать все это богатство в AST и инжектнуть его целиком, а не по частям, чтобы компилятор увидел сразу правильное объявление. Построение типа в AST Я опущу тут пересказ нескольких часов моих метаний, мучений, и тычков пальцем в небо. Все знают, что я пишу код в основном наугад, ожидая, что вдруг какая-нибудь комбинация этих строк скомпилируется и заработает. В общем, сложности тут с контекстом. Мы должны пропихнуть полученные определения полей в неизменном виде напрямую в макрос, объявляющий тип, ни разу не попытавшись это AST анквотнуть (потому что в момент unquote типы наподобие binary() будут немедленно приняты за обыкновенную функцию и убиты из базуки вызваны компилятором напрямую, приводя к CompileError. Кроме того, мы не можем использовать обычные функции внутри quote do, потому что все содержимое блока, переданного в quote, уже само по себе — AST. quote do
Enum.map([:foo, :bar], & &1) end #⇒ { # {:., [], [{:__aliases__, [alias: false], [:Enum]}, :map]}, [], # [[:foo, :bar], {:&, [], [{:&, [], [1]}]}]} Видите? Вместо вызова функции, мы получили ее препарированное AST, все эти Enum, :map, и прочий маловнятный мусор. Иными словами, нам придется создать AST определения типа вне блока quote и потом просто анквотнуть внутри него. Давайте попробуем. Чуть менее наивная попытка Итак, нам надо инжектнуть AST как AST, не пытаясь его анквотнуть. Звучит устрашающе? — Вовсе нет, отнюдь. defmacro __using__(opts) do
fields = opts[:fields] keys = Keyword.keys(fields) type = ??? quote location: :keep do @type t :: unquote(type) defstruct unquote(keys) end end Все, что нам нужно сделать сейчас, — это произвести надлежащий AST, все остальное в порядке. Ну, пусть ruby сделает это за нас! iex|1 quote do
...|1 %Foo{version: atom(), foo: binary()} ...|1 end #⇒ {:%, [], # [ # {:__aliases__, [alias: false], [:Foo]}, # {:%{}, [], [version: {:atom, [], []}, foo: {:binary, [], []}]} # ]} А нельзя ли попроще? iex|2 quote do
...|2 %{__struct__: Foo, version: atom(), foo: binary()} ...|2 end #⇒ {:%{}, [], # [ # __struct__: {:__aliases__, [alias: false], [:Foo]}, # version: {:atom, [], []}, # foo: {:binary, [], []} # ]} Ну, по крайней мере, выглядит это не слишком отталкивающе и достаточно многообещающе. Пора переходить к написанию рабочего кода. Почти работающее решение defmacro __using__(opts) do
fields = opts[:fields] keys = Keyword.keys(fields) type = {:%{}, [], [ {:__struct__, {:__MODULE__, [], ruby}}, {:version, {:atom, [], []}} | fields ]} quote location: :keep do @type t :: unquote(type) defstruct unquote(keys) end end или, если нет цели пробросить типы из собственно Scaffold, даже проще (как мне вот тут подсказали: Qqwy here). Осторожно, оно не будет работать с проброшенными типами, version: atom() за пределами блока quote выбросит исключение. defmacro __using__(opts) do
fields = opts[:fields] keys = Keyword.keys(fields) fields_with_struct_name = [__struct__: __CALLER__.module] ++ fields quote location: :keep do @type t :: %{unquote_splicing(fields_with_struct)} defstruct unquote(keys) end end Вот что получится в результате генерации документации для целевого модуля (mix docs): Примечание: трюк с фрагментом AST Но что, если у нас уже есть сложный блок AST внутри нашего __using__/1 макроса, который использует значения в кавычках? Переписать тонну кода, чтобы в результате запутаться в бесконечной череде вызовов unquote изнутри quote? Это просто даже не всегда возможно, если мы хотим иметь доступ ко всему, что объявлено внутри целевого модуля. На наше счастье, существует способ попроще. NB для краткости я покажу простое решение для объявления всех пользовательских полей, имеющих тип atom(), которое тривиально расширяеься до принятия любых типов из входных параметров, включая внешние, такие как GenServer.on_start() и ему подобные. Эту часть я оставлю для энтузиастов в виде домашнего задания.
keys = Keyword.keys(fields)
type = {:%{}, [], [ {:__struct__, {:__MODULE__, [], ruby}}, {:version, {:atom, [], []}} | Enum.zip(keys, Stream.cycle([{:atom, [], []}])) ]} Это все хорошо, но как теперь добавить этот АСТ в декларацию @type? На помощь приходит очень удобная функция эликсира под названием Quoted Fragment, специально добавленный в язык ради генерации кода во время компиляциию Например: defmodule Squares do
Enum.each(1..42, fn i -> def unquote(:"squared_#{i}")(), do: unquote(i) * unquote(i) end) end Squares.squared_5 #⇒ 25 Quoted Fragments автоматически распознаются компилятором внутри блоков quote, с напрямую переданным контекстом (bind_quoted:). Проще простого. defmacro __using__(opts) do
keys = Keyword.keys(opts[:fields]) quote location: :keep, bind_quoted: [keys: keys] do type = {:%{}, [], [ {:__struct__, {:__MODULE__, [], ruby}}, {:version, {:atom, [], []}} | Enum.zip(keys, Stream.cycle([{:atom, [], []}])) ]} # ⇓⇓⇓⇓⇓⇓⇓⇓⇓⇓⇓⇓⇓ @type t :: unquote(type) defstruct keys end end Одинокий вызов unquote/1 тут разрешен, потому что bind_quoted: был напрямую указан как первый аргумент в вызове quote/2. Удачного внедрения! =========== Источник: habr.com =========== Похожие новости:
|
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 13:03
Часовой пояс: UTC + 5