Эликсир миксин используется для проверки обратного вызова - PullRequest
0 голосов
/ 31 января 2019

Я читал другие ответы SO по этому вопросу, но я специально хотел сосредоточиться на собственной документации Elixir , в которой говорится о миксинах и DSL.

В своем примере они говорят, что ониЕсть три варианта:

# 1. data structures
import Validator
validate user, name: [length: 1..100],
               email: [matches: ~r/@/]

# 2. functions
import Validator
user
|> validate_length(:name, 1..100)
|> validate_matches(:email, ~r/@/)

# 3. macros + modules
defmodule MyValidator do
  use Validator
  validate_length :name, 1..100
  validate_matches :email, ~r/@/
end

MyValidator.validate(user)

Конечно, примеры не полны.Поэтому я решил попробовать стиль № 3, чтобы лучше понять некоторые библиотеки, которые я использую, которые используют некоторые из этих стилей (не обязательно стиль № 3).

Есть некоторая ошибка с попыткамичтобы сделать это в файле test.exs, потому что структура еще не была определена или к структуре обращаются в том же контексте, который ее определяет , но обход этого с помощью модуля Main приводит меня к чему-то вроде этого:

defmodule Validator do

  defmacro __using__(_params) do
    quote do
      def validate_length(field, length_rules) do
        String.length(field) >= length_rules.first and String.length(field) <= length_rules.last
      end
    end
  end

end

defmodule MyValidator do
  use Validator
  validate_length :name, 1..10
end

defmodule User do
  @enforce_keys [:name]
  defstruct [:name]
end

defmodule Main do
  def run do
    user = %User{name: "Joe"}
    MyValidator.validate(user)
  end
end

Main.run
# undefined function validate_length/2

Я бы не ожидал, что это сработает, потому что я не понимаю связи между MyValidator и "обратными вызовами" для того, что означает проверка.Должен ли я посмотреть (метапрограмму), какие проверки были использованы?Должен ли я реализовать _params, чтобы увидеть, какие обратные вызовы будут использоваться в будущем?

Несмотря на это, ошибка составляет undefined function validate_length/2, поскольку между ними нет "разводки".Конечно, если я изменю основной на что-то вроде:

defmodule MyValidator do
  use Validator
end

def run do
  user = %User{name: "Joe"}
  MyValidator.validate_length(user.name, 1..10)

Тогда, конечно, это работает, но это не обратный вызов, это просто миксин.

Так как же вы можете закончить?пример № 3 для Elixir, действующий как миксин, который может иметь много валидаторов, много обратных вызовов?

1 Ответ

0 голосов
/ 01 февраля 2019

Вы должны начать со строгого разъединения двух контекстов: время компиляции контекст против время выполнения контекст.

Для этого можно начать со следующего примера:

defmodule Foo do
  IO.puts "Compilation"

  def bar(), do: IO.puts "Runtime"
end

Когда Elixir компилирует это, оно печатает первое.Когда вы звоните Foo.bar/0, вы напечатаете последнее.


Теперь к макросам.Макросы время компиляции звери.На этапе компиляции Elixir принимает AST, который возвращает макрос, и явно внедряет его в место, откуда был вызван макрос.Рассмотрим следующий пример.

defmodule Foo do
  defmacro bar() do
    IO.puts "Compilation"
    quote do: IO.puts "Runtime"
  end

  def baz(), do: bar()
end

Попробуйте скомпилировать его и затем вызвать Foo.baz/0.Дело в том, что язык макросов Elixir равен Elixir , и компилятор прекрасно справляется с выполнением кода при переходе макросов в AST.Вот почему вы печатаете "Compilation" строку только один раз.После того как компиляция пройдена, прежний IO.puts/2 вызов больше не существует .


Теперь ваш пример.Во-первых, не пытайтесь поместить код, который должен быть скомпилирован в *.exs.s означает script , и эти файлы не скомпилированы во время обычной стадии компиляции по умолчанию.Есть много причин для этого, но они определенно выходят за рамки здесь.Итак, поместите код в отдельные файлы с расширением *.ex.

Важно: вы хотите, чтобы функция validate/1 была доступна в время компиляции .

Итак, use Validator должен a) ввестикод этой функции и б) импортировать ее в текущий контекст во время компиляции.Чтобы он был доступен на этапе компиляции, он должен находиться в другом модуле , поскольку этот модуль должен быть уже скомпилирован ко времени внедрения. Elixir не является языком сценариев и не может запускать не скомпилированный код.

Суммирование.

defmodule Validator do
  defmacro __using__(_) do
    quote do
      # you need this module IMPORTED
      import Validator
    end
  end
  # you need this function COMPILED
  def validate(foo) do
    if foo > 42,
      do: raise(ArgumentError, "FOO"),
      else: IO.puts("OK")
  end
end

defmodule Test do
  use Validator

  validate 0    # prints out "OK"
  validate 100  # raises _during compilation_
end

Для более сложных проверок * Функция 1064 *может ввести время выполнения AST.

...