【问题标题】:Using guards in Elixir macros在 Elixir 宏中使用守卫
【发布时间】:2018-02-10 00:16:36
【问题描述】:

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

这个:

  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 以使其与具有任意数量警卫的函数一起使用?

【问题讨论】:

    标签: macros elixir


    【解决方案1】:

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

    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 子句。)

    【讨论】:

    • 你的答案更好:)
    • 这几乎是好的。但是我的问题是如何使这个宏与具有 any 数量的警卫的函数一起工作:) 我可以对Macro.decompose_call(ast) 的结果进行一些复杂的递归模式匹配。但是我正在寻找更简单的方法来实现这一目标
    • “任意数量的守卫”是什么意思?一个函数只能有 1 个守卫。
    • @whysoserious 上面的内容非常适合它,您可以自己检查一下。第二个whenand 的别名。
    • 是的!按预期工作。谢谢!
    【解决方案2】:

    defstate 的调用在编译时扩展为来自defmacroquote 块中的内容。因此,保护​​表达式不会直接应用于宏调用,因为在编译时,您在内部定义的函数不会被调用。

    所以你必须自己获取 :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 放在参数列表的第一项中,并将保护表达式放在第二项中。

    请注意,如果您想在没有保护子句的情况下使用宏,您仍然必须在此定义下定义一个包罗万象的宏定义。

    【讨论】:

      猜你喜欢
      • 2015-05-15
      • 2015-08-15
      • 2021-06-03
      • 1970-01-01
      • 1970-01-01
      • 2011-09-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多