【问题标题】:Recursive macro Elixir递归宏 Elixir
【发布时间】:2020-05-25 20:25:43
【问题描述】:

我在玩 Elixir 宏 - 特别是自称宏,这是我在 Scheme 中经常做的事情。我在下面创建了一个小测试宏,但它只是挂起 iex - 没有任何内容打印到控制台。有没有人知道为什么以及可以做些什么来纠正它?

defmodule RecMac do
  defmacro test_rec(x) do
    quote do
      IO.puts("Started?")
      if(unquote(x) < 1) do
        IO.puts("Done?")
        "done"
      else
        IO.puts("Where are we")
        IO.puts(unquote(x))
        RecMac.test_rec(unquote(x) - 1)
      end
    end
  end
end

编辑!!

好的,事实证明你可以定义递归宏,其中存在要匹配的结构差异(例如列表)。以下对我有用。并在下面确认@Aleksei Matiushkin,上面的方法不起作用,而且在方案中确实不起作用!

  defmacro test_rec([h | t]) do
    quote do
      IO.inspect([unquote(h) | unquote(t)])
      RecMac.test_rec(unquote(t))
    end
  end

  defmacro test_rec([]) do
    quote do
      IO.puts "Done"
    end
  end
end

我很高兴能够深入研究这一点,因为我学到了两种语言的知识!

【问题讨论】:

    标签: macros elixir


    【解决方案1】:

    TL;DR:这是不可能的。


    中的宏不是您期望的那样。当编译器看到一个宏时,它会在编译期间调用它 并且 注入它返回的AST来代替它被调用的地方。也就是说,递归宏总是会在编译阶段导致无限循环。

    IO.puts("something") 放在 quote do 指令之前,您会看到它被无限打印,每次后续调用扩展宏一次。

    您可以使用@compile {:inline, test_rec: 1} 来实现您所追求的行为。为了更好地理解宏,您可能应该阅读 Elixir 指南 中的 Macros 部分,尤其是以下摘录:

    宏比普通的 Elixir 函数更难编写,并且在不需要时使用它们被认为是不好的风格。所以要负责任地编写宏。

    【讨论】:

    • 感谢您的回答,这很公平,这在 Elixir 中是不可能的,但在 Scheme 中肯定是可能的,这让我认为这是 Elixir 宏的一个缺点:-(
    • scheme 一开始就不是编译语言(chicken 之类的东西应该称为编译器。)Elixir 编译成 BEAM,并且发明了宏来简化 AST 的遍历。这是一个特点,无论如何都不是缺点。如果您认为宏应该始终与 scheme 中的宏类似,请随意将它们称为 ast-injectors 而不是 macros
    • github.com/cisco/chezscheme 肯定是编译的。不是在这里争论 :-) 再次感谢您的回答 - 无法在 Elixir 中执行此操作对我来说并不是一个交易破坏者,该语言在其他方面非常高效,而且很有趣。
    【解决方案2】:

    实际上你可以做某种递归,但重点是想想你在做什么以避免在quote do [....] end 中调用宏。您的 IO.puts 示例将是这样的

    defmodule RecMac  do
      defmacro test_rec(list) do
        {:__block__, [], quote_value(list)}
      end
    
      defp quote_value([]), do: []
      defp quote_value([h | t]) do
        if h < 1 do
          []
        else
          ast = quote do
             IO.puts("#{unquote(h)}")
          end
          [ast | quote_value(t)]
        end
      end
    end
    

    您会注意到两件事,我确实在宏中使用递归,但在外部引用使用私有函数 quote_value/1,并且该函数具有在找到低于 1 的值后“停止”的逻辑。所有“引号”都放入列表中,诀窍是将此列表放入元组{:__block__, [], put_quote_list_here}

    现在请注意,如果预先不知道 test_rec 的列表参数(在编译期间),则此宏不会编译,因此您需要调用宏 test_rec(["a", "b", 0, 100, 200]) 以便编译器知道该列表的大小和元素。

    顺便说一句,我使用了体优化递归,但您可以轻松添加累加器并将其转换为尾优化递归。

    【讨论】:

    • 他最好不要使用这个,imo。
    • @Milan Jaric 感谢您思考一段非常有趣的代码!我只是看看 Elixir 宏可以做什么和不可以做什么,所以这很棒:-)
    • @Daniel 我同意,因为它与在运行时直接调用 quote_value 完全没有区别。但是如果你有更复杂的东西,例如如果你想为某个域构建 DSL,那么做这样的事情是有意义的,而不是使用 IO.puts,只需添加应该可以工作的代码而不是 DSL 关键字
    猜你喜欢
    • 2019-08-25
    • 2019-03-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-06-17
    相关资源
    最近更新 更多