用于验证回调的Elixir Mixins

时间:2019-01-31 20:40:15

标签: elixir

我已经阅读了有关该主题的其他SO答案,但我特别想重点关注Elixir自己的documentation,它讨论了mixin和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)

那当然可以,但是不执行回调,这只是一个混合操作。

那么您如何才能完成Elixir示例#3,使其像可以有许多验证器,许多回调的Mixin一样起作用?

1 个答案:

答案 0 :(得分:1)

您应该从严格取消两个上下文开始:编译时间上下文与运行时上下文。

为此,可以从以下示例开始:

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)注入该函数的代码,b)在编译期间将其导入当前上下文。为了使其在编译阶段可用,它应该驻留在不同模块中,因为该模块必须在注入时已被编译。 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

要进行更复杂的检查,validate函数可能会注入运行时AST。