在Elixir宏中使用警卫

时间:2018-02-09 15:32:24

标签: macros elixir

我正在研究宏,它将采用一个函数并添加一些额外的功能。例如:

此:

  defstate this_works(a, b) do
    a + b + 1
  end

应转换为:

  def this_works(a, b) do
    IO.puts("LOGGING whatever")
    a + b + 1
  end

这是我到目前为止所拥有的。尝试在iex中运行这段代码:

  defmodule MyMacro do
    defmacro defstate(ast, do: block) do
      {fn_atom, _} = Macro.decompose_call(ast)

      quote do
        def unquote(fn_atom)(var!(a), var!(b)) do
          IO.puts("LOGGING")
          unquote(block)
        end
      end
    end
  end

  defmodule Test1 do
    import MyMacro

    defstate this_works(a, b) do
      a + b + 1
    end
  end

  Test.this_works(1, 2)

这可以按预期工作。

现在,这个模块没有编译:

  defmodule Test2 do
    import MyMacro

    defstate this_fails(a, b) 
      when 1 < 2 
      when 2 < 3 
      when 3 < 4 do
      a + b + 1
    end
  end

唯一的变化是我添加了一名后卫,而宏无法处理。

如何改进MyMacro.defstate以使其适用于具有任意数量警卫的功能?

2 个答案:

答案 0 :(得分:3)

如果您使用fn_atom检查defstate this_fails(a, b) when 1 < 2,则会看到它是:when而不是:this_fails。这是因为Elixir AST中when表达式的表示方式:

iex(1)> quote do
...(1)>   def foo, do: 1
...(1)> end
{:def, [context: Elixir, import: Kernel],
 [{:foo, [context: Elixir], Elixir}, [do: 1]]}
iex(2)> quote do
...(2)>   def foo when 1 < 2, do: 1
...(2)> end
{:def, [context: Elixir, import: Kernel],
 [{:when, [context: Elixir],
   [{:foo, [], Elixir}, {:<, [context: Elixir, import: Kernel], [1, 2]}]},
  [do: 1]]}

你可以使用一些模式匹配来解决这个问题:

defmodule MyMacro do
  defmacro defstate(ast, do: block) do
    f = case ast do
      {:when, _, [{f, _, _} | _]} -> f
      {f, _, _} -> f
    end

    quote do
      def unquote(ast) do
        IO.puts("LOGGING #{unquote(f)}")
        unquote(block)
      end
    end
  end
end

defmodule Test do
  import MyMacro

  defstate this_works(a, b) do
    a + b + 1
  end

  defstate this_works_too(a, b) when a < 2 do
    a + b + 1
  end
end

defmodule A do
  def main do
    IO.inspect Test.this_works(1, 2)
    IO.inspect Test.this_works_too(1, 2)
    IO.inspect Test.this_works_too(3, 2)
  end
end

A.main

输出:

LOGGING this_works
4
LOGGING this_works_too
4
** (FunctionClauseError) no function clause matching in Test.this_works_too/2

    The following arguments were given to Test.this_works_too/2:

        # 1
        3

        # 2
        2

    a.exs:24: Test.this_works_too/2
    a.exs:33: A.main/0
    (elixir) lib/code.ex:376: Code.require_file/2

(我还在def之后更改了取消引用,以确保保留when子句。)

答案 1 :(得分:0)

defstate的调用在编译时从quote扩展到defmacro块中的内容。因此,保护​​表达式不会直接应用于宏调用,因为在编译时,不会调用您在里面定义的函数。

所以你必须自己抓住:when元组并自己添加警卫:

defmodule MyMacro do
  defmacro defstate({:when, _, [ast, guards]}, do: block) do
    {fn_atom, _} = Macro.decompose_call(ast)

    quote do
      def unquote(fn_atom)(var!(a), var!(b)) when unquote(guards) do
        IO.puts("LOGGING")
        unquote(block)
      end
    end
  end
end

注意我现在如何匹配{:when, _, [ast, guards]}元组。

当你用一个守卫调用一个宏时,它会将原始的ast放在参数列表的第一个项目中,并将守护表达式放在第二个项目中。

请注意,如果你想使用没有保护条款的宏,你仍然需要在这个下面定义一个全能宏定义。