宏代码转换的透析问题

Dialyxir issue with macro code transformation

提问人:denis.peplin 提问时间:11/3/2023 更新时间:11/3/2023 访问量:25

问:

我正在尝试构建一个宏,将 Elixir 代码转换为某种“配置”以供以后使用,所以这段代码:

    build_config with one_and_two <- SomeModule.run("one", "two"),
                      _three_and_four <- SomeModule.run(one_and_two, "four"),
                      _six_and_five <- SomeModule.run("six", "five") do
      IO.puts(one_and_two)
    end

将被表示为

[
  {:one_and_two, SomeModule, :run, ["one", "two"]},
  {:_three_and_four, SomeModule, :run, [{:from, :one_and_two}, "four"]},
  {:_six_and_five, SomeModule, :run, ["six", "five"]}
]

这种方法的问题在于,当我运行时,它不会抱怨明显不好的类型,但是当我运行常规代码时,它确实会抱怨。mix dialyzer

例如,我在 SomeModule 中有以下代码:

defmodule SomeModule do
  @spec run(binary, atom) :: binary
  def run(arg1, arg2) do
    IO.puts("RUN FOR #{arg1} AND #{arg2}")
    arg1 <> Atom.to_string(arg2)
  end
end

让 Dialyzer 抱怨的常规代码是这样的:

one_and_two = SomeModule.run("one", "two")
SomeModule.run(one_and_two, "four")

我试图过度简化我构建的宏,但它仍然有点太复杂了:

  defmacro build_config({:with, _meta, args}, do: _block) do
    {lines, _variables_map} =
      Enum.map_reduce(args, MapSet.new(), fn arg, names ->
        {:<-, _,
         [
           {name, _, nil},
           {{:., _, [{:__aliases__, _, [_module]}, _function]} = call, meta, variables}
         ]} = arg

        names = MapSet.put(names, name)

        new_variables =
          Enum.map(variables, fn
            {variable_name, _, nil} = variable ->
              if variable_name in names do
                {:from, variable_name}
              else
                variable
              end

            other ->
              other
          end)

        {module, function, params} = Macro.decompose_call({call, meta, new_variables})

        line =
          quote do
            {unquote(name), unquote(module), unquote(function), unquote(params)}
          end

        {line, names}
      end)

    lines
  end

Github 上的存储库包含示例代码:https://github.com/denispeplin/config_generator

我的问题不是关于宏本身,而是关于整个方法:如果宏输出的东西不是可执行的“代码”,而是某种“配置”,它本身不调用任何东西,Dialyzer应该抱怨类型错误吗?

有什么方法可以实现我想要的:从代码生成配置并让 Dialyzer 同时工作?

Elixir 透析器 Erlang

评论


答:

1赞 Aleksei Matiushkin 11/3/2023 #1

上面宏的 MRE 可以简化为返回元组的单行代码。一般来说,这个问题与宏无关,也与它的返回类型无关。宏确实在适当的位置注入了 AST,没有魔法,透析器对宏一无所知,它可以与注入的代码一起使用。

宏结果只是一个元组。你没有展示如何使用这个元组,但我猜它会通过 Kernel.apply/3,它有一个类型作为第三个参数。list()

中没有这样的类型(因此在 中)作为具有指定类型的固定长度列表。这就是为什么无法检查上面介绍的方法中参数列表中的元素的原因。


不过,您可以做的是引入您自己的宏,这将扩展 unquote_splicing/1 的参数。然后,注入的代码将类似于常规代码,参数逐个传递,而不是作为列表传递,因此透析器将能够检查它们的类型。apply/3


我的建议是避免走这条路,直到你对宏的工作原理有绝对清楚的了解,并且使用不那么麻烦的东西。

评论

0赞 denis.peplin 11/7/2023
它的使用方式已经在我正在从事的项目中,所以我不能避免这条路。这是一些遗留的解决方案,我试图用手将宏放入代码中来生成“配置”。这不是一个简单的库,它是一个执行异步魔术的库,并接受一些我没有包含在问题中的额外参数。我试图通过这个宏实现两件事:代码导航(它有效!)和透析器功能(我会尝试您建议unquote_splicing方法)。apply
1赞 Aleksei Matiushkin 11/7/2023
好吧,您可能会疯狂地使用未记录的 Code.Typespec.fetch_types/1 来自己验证类型。请不要告诉任何人我建议的:)