Русский
Русский
English
Статистика
Реклама

Recovery mode Типы, где их не ждали

Давайте представим себе реализацию модуля Scaffold, который генерирует структуру с предопределенными пользовательскими полями и инжектит ее в вызываемый модуль при помощи use Scaffold. При вызове use Scaffold, fields: foo: [custom_type()], ... мы хотим реализовать правильный тип в Consumer модуле (common_field в примере ниже определен в Scaffold или еще где-нибудь извне).


@type t :: %Consumer{  common_field: [atom()],  foo: [custom_type()],  ...}

Было бы круто, если бы мы могли точно сгенерировать тип Consumer.t() для дальнейшего использования и создать соответствующую документацию для пользователей нашего нового модуля.


Lighthouse in French Catalonia


Пример посложнее будет выглядеть так:


defmodule Scaffold do  defmacro __using__(opts) do    quote do      @fields unquote(opts[:fields])      @type t :: %__MODULE__{        version: atom()        # magic      }      defstruct @fields    end  endenddefmodule 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|aend

Выглядит несложно, да?


Наивный подход


Давайте начнем с анализа того, что за 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)  endend

Все, что нам нужно сделать сейчас, это произвести надлежащий 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)  endend

или, если нет цели пробросить типы из собственно 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)  endend

Вот что получится в результате генерации документации для целевого модуля (mix docs):


Screenshot of type definition


Примечание: трюк с фрагментом 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)endSquares.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  endend

Одинокий вызов unquote/1 тут разрешен, потому что bind_quoted: был напрямую указан как первый аргумент в вызове quote/2.




Удачного внедрения!

Источник: habr.com
К списку статей
Опубликовано: 12.11.2020 08:22:24
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Open source

Erlang/otp

Elixir/phoenix

Macros

Macro

Injection

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru