简体   繁体   English

如何在Elixir中扩展多个宏?

[英]How to expand multiple macros in Elixir?

I'm starting my adventure with Elixir and I need a little bit o help. 我从Elixir开始冒险,需要一点帮助。

I'm trying to simplify my structs definition and validation by using macros. 我正在尝试通过使用宏简化结构的定义和验证。 The goal is to automatically inject defstruct and Vex library validators based on provided options in modules using it. 目标是根据使用defstruct和Vex库验证程序的模块中提供的选项自动注入它。

I've come up with the code as follows: 我提出了如下代码:

defmodule PdfGenerator.BibTypes.TypeDefinition do
  @callback valid?(%{}) :: boolean

  defmacro __using__(mod: mod, style: style, required: required, optional: optional) do
    required_props = required |> Enum.map(&{:"#{&1}", nil})
    optional_props = optional |> Enum.map(&{:"#{&1}", nil})

    quote location: :keep do
      defstruct unquote([{:style, style}] ++ required_props ++ optional_props)
      @behaviour PdfGenerator.BibTypes.TypeDefinition
      use Vex.Struct

      def cast(%{} = map) do
        styled_map = Map.put(map, :style, unquote(style))
        struct_from_map(styled_map, as: %unquote(mod){})
      end

      defp struct_from_map(a_map, as: a_struct) do
        keys =
          Map.keys(a_struct)
          |> Enum.filter(fn x -> x != :__struct__ end)

        processed_map =
          for key <- keys, into: %{} do
            value = Map.get(a_map, key) || Map.get(a_map, to_string(key))
            {key, value}
          end

        a_struct = Map.merge(a_struct, processed_map)
        a_struct
      end

      validates(
        :style,
        presence: true,
        inclusion: [unquote(style)]
      )
    end

    Enum.each(required, fn prop ->
      quote location: :keep do
        validates(
          unquote(prop),
          presence: true
        )
      end
    end)
  end
end

And I'm using this macro in another module: 我在另一个模块中使用此宏:

defmodule PdfGenerator.BibTypes.Booklet do
  use PdfGenerator.BibTypes.TypeDefinition,
    mod: __MODULE__,
    style: "booklet",
    required: [:title],
    optional: [:author, :howpublished, :address, :month, :year, :note]
end

I want PdfGenerator.BibTypes.Booklet module, after macro expansion, to look as follows: 我希望宏扩展后的PdfGenerator.BibTypes.Booklet模块如下所示:

defmodule PdfGenerator.BibTypes.Booklet do
  defstruct style: "booklet",
            title: nil,
            author: nil,
            howpublished: nil,
            address: nil,
            month: nil,
            year: nil,
            note: nil

  @behaviour PdfGenerator.BibTypes.TypeDefinition
  use Vex.Struct

  def cast(%{} = map) do
    styled_map = Map.put(map, :style, "booklet")
    struct_from_map(styled_map, as: %PdfGenerator.BibTypes.Booklet{})
  end

  defp struct_from_map(a_map, as: a_struct) do
    keys =
      Map.keys(a_struct)
      |> Enum.filter(fn x -> x != :__struct__ end)

    processed_map =
      for key <- keys, into: %{} do
        value = Map.get(a_map, key) || Map.get(a_map, to_string(key))
        {key, value}
      end

    a_struct = Map.merge(a_struct, processed_map)
    a_struct
  end

  validates(
    :style,
    presence: true,
    inclusion: ["booklet"]
  )

  validates(
    :title,
    presence: true
  )
end

As you can see, based on required option, I'm trying to expand to Vex -specific macro (which in turn should be expanded further on in Vex.Struct macro definition) validates(:<PROP_NAME>, presence: true) for every value in required list. 如您所见,基于required选项,我试图扩展到Vex特定的宏(依次应在Vex.Struct宏定义中进一步扩展) validates(:<PROP_NAME>, presence: true) required列表中的值。 This macro code works (but without these validators for required values) when I remove last block from __using__ macro: 当我从__using__宏中删除最后一个块时,此宏代码有效(但没有用于所需值的这些验证器):

Enum.each(required, fn prop ->
  quote location: :keep do
    validates(
      unquote(prop),
      presence: true
    )
  end
end)

But with it, when I'm trying to issue following command in the iex console: %PdfGenerator.BibTypes.Booklet{} 但是有了它,当我尝试在iex控制台中发出以下命令时: %PdfGenerator.BibTypes.Booklet{}

I get: 我得到:

** (CompileError) iex:1: PdfGenerator.BibTypes.Booklet.__struct__/1 is undefined, cannot expand struct PdfGenerator.BibTypes.Booklet

Any idea, what am I doing wrong? 任何想法,我在做什么错? Any hint would be greatly appreciated as I'm pretty new to the whole Elixir and macros world. 任何提示将不胜感激,因为我对整个Elixir和宏世界还很陌生。

Since you did not provide the MCVE , it's extremely hard to test the solution, but at the first glance the issue is you expect some magic from Kernel.SpecialForms.quote/2 , while it does not implicitly inject anything anywhere , it just produces an AST . 由于您未提供MCVE ,因此很难测试解决方案,但是乍一看,问题在于您希望Kernel.SpecialForms.quote/2一些魔力,尽管它不会在任何地方隐式注入任何内容 ,但只会产生一个AST

When you call 你打电话时

Enum.each(...)

as the last line of quote do block, the result of this call is returned as AST from quote do . 作为quote do块的最后一行,此调用的结果从quote do 作为AST返回 That said, the current __using__ implementation injects the result of the call to quote do: :ok , which is apparently :ok . 也就是说,当前的__using__实现会注入对quote do: :ok的调用结果quote do: :ok ,显然是:ok What you need, is to build the list of clauses to be injected: 您需要的是构建要注入的子句的列表:

defmacro __using__(mod: mod, ...) do
  # preparation
  ast_defstruct =
    quote location: :keep do
      # whole stuff for defstruct
    end

  # NB! last term will be returned from `__using__`!
  [
    ast_defstruct |
    Enum.map(required, fn prop ->
      quote location: :keep,
        do: validates(unquote(prop), presence: true)
    end)
  ]

By using Enum.map/2 we collect quoted ASTs for each element, and append them to already built AST for defstruct creation. 通过使用Enum.map/2我们为每个元素收集带引号的AST, 并将它们附加到已经构建的AST中以进行defstruct创建。 We return a list (which is a proper AST,) containing many clauses. 我们返回一个包含许多子句的列表(这是一个适当的AST)。

Still, I am unsure if this is the only glitch due to lack of MCVE, but this is definitely the proper fix to start with. 不过,我不确定这是否是由于缺少MCVE而造成的唯一故障,但这绝对是开始的正确方法。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM