【问题标题】:How to expand multiple macros in Elixir?如何在 Elixir 中扩展多个宏?
【发布时间】:2018-01-18 15:05:45
【问题描述】:

我正在使用 Elixir 开始我的冒险,我需要一点帮助。

我正在尝试通过使用宏来简化我的结构定义和验证。目标是根据使用它的模块中提供的选项自动注入 defstruct 和 Vex 库验证器。

我想出了如下代码:

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

我正在另一个模块中使用这个宏:

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

我希望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

如您所见,基于required 选项,我正在尝试扩展为Vex 特定的宏(反过来应该在Vex.Struct 宏定义中进一步扩展)validates(:&lt;PROP_NAME&gt;, presence: true) 的每个值在required 列表中。 当我从__using__ 宏中删除最后一个块时,此宏代码有效(但没有这些验证器来获取所需值):

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

但是,当我尝试在iex 控制台中发出以下命令时:%PdfGenerator.BibTypes.Booklet{}

我明白了:

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

任何想法,我做错了什么?任何提示将不胜感激,因为我对整个 Elixir 和宏世界都很陌生。

【问题讨论】:

  • 没有 MCVE--downvoted。

标签: macros elixir


【解决方案1】:

由于您没有提供MCVE,因此很难测试该解决方案,但乍一看,问题是您期望Kernel.SpecialForms.quote/2 有一些魔力,而它并没有在任何地方隐式注入任何东西,它只是产生一个 AST

当你打电话时

Enum.each(...)

作为quote do 块的最后一行,此调用的结果quote do 返回为AST。也就是说,当前的__using__ 实现将调用结果注入quote do: :ok,显然是:ok。您需要的是构建要注入的子句列表:

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)
  ]

通过使用Enum.map/2,我们收集引用了每个元素的AST,并将它们附加到已经构建的AST以创建defstruct。我们返回一个包含许多子句的列表(这是一个正确的 AST)。

不过,我不确定这是否是由于缺少 MCVE 而导致的唯一故障,但这绝对是开始的正确修复。

【讨论】:

  • 是的,就是这样。我忘记了一个非常简单的事实:Elixir 中的每个函数都会返回一些东西。非常感谢您的帮助和投入的时间:)
猜你喜欢
  • 2016-03-21
  • 2020-05-29
  • 1970-01-01
  • 2019-09-23
  • 2022-01-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多