简体   繁体   English

在Elixir宏中使用警卫

[英]Using guards in Elixir macros

I am working on macro which would take a function and add some additional functionality. 我正在研究宏,它将采用一个函数并添加一些额外的功能。 Eg.: 例如。:

This: 这个:

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

Should be converted to this: 应转换为:

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

This is what I have so far. 这就是我到目前为止所拥有的。 Try running this piece of code in iex: 尝试在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)

This works as expected. 这按预期工作。

Now, this module does not compile: 现在,这个模块没有编译:

  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

The only change is that I added a guard and macro is unable to deal with that. 唯一的变化是我添加了一个后卫,宏无法处理。

How can I improve MyMacro.defstate to make it work with a function with any number of guards? 如何改进MyMacro.defstate以使其适用于具有任意数量防护的功能?

If you inspect fn_atom with the defstate this_fails(a, b) when 1 < 2 , you'll see that it's :when instead of :this_fails . 如果你defstate this_fails(a, b) when 1 < 2defstate this_fails(a, b) when 1 < 2检查fn_atom ,你会看到它是:when而不是:this_fails This is because of how when expressions are represented in the Elixir AST: 这是因为如何when表达的药剂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]]}

You can fix this using some pattern matching: 您可以使用一些模式匹配来解决此问题:

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

Output: 输出:

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

(I also changed the unquote after def to make sure the when clause is preserved.) (我还在def之后更改了unquote,以确保保留when子句。)

The call to defstate is expanded at compile time to the things in the quote block from your defmacro . defstate的调用在编译时扩展到defstate中的quote块中的defmacro As such, guard expressions will not be applied to the macro call directly, because at compile time, the function you're defining inside is not called. 因此,保护​​表达式不会直接应用于宏调用,因为在编译时,不会调用您在里面定义的函数。

So you have to grab the :when tuple yourself and add the guards yourself: 所以你必须抓住: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

Note how I match for a {:when, _, [ast, guards]} tuple now. 注意我现在如何匹配{:when, _, [ast, guards]}元组。

When you call a macro with a guard, it will put the original ast inside the first item of the arguments list, and the guard expression inside the second item. 当你用一个守卫调用一个宏时,它会将原始的ast放在参数列表的第一项中,并将第二项中的守护表达式放在内。

Note that you'll still have to define a catch-all macro definition below this one in case you want to use your macro without guard clauses. 请注意,如果您想使用没有保护子句的宏,您仍然需要在此下面定义一个全能宏定义。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM