Macros 使用\uuuu宏在\uuu中添加默认句柄\u信息

Macros 使用\uuuu宏在\uuu中添加默认句柄\u信息,macros,metaprogramming,elixir,Macros,Metaprogramming,Elixir,我正试图做一个小包装,但我遇到了一个小问题使用宏将ast附加到模块的开头,我想将函数定义作为默认值附加到模块的结尾。我可能会在使用该包装器的每个模块中手动执行该操作,但我非常确定我会在某个时候忘记这一点 我当前的包装器实现: defmodule Cgas.IrcServer do defmacro __using__(opts) do quote do use GenServer alias ExIrc.Client require Logger

我正试图做一个小包装,但我遇到了一个小问题<代码>使用宏将ast附加到模块的开头,我想将函数定义作为默认值附加到模块的结尾。我可能会在使用该包装器的每个模块中手动执行该操作,但我非常确定我会在某个时候忘记这一点

我当前的包装器实现:

defmodule Cgas.IrcServer do
  defmacro __using__(opts) do
    quote do
      use GenServer
      alias ExIrc.Client
      require Logger
      defmodule State do
        defstruct host: "irc.chat.twitch.tv",
          port: 6667,
          pass: unquote(Keyword.get(opts, :password, "password")),
          nick: unquote(Keyword.get(opts, :nick, "uname")),
          client: nil,
          handlers: [],
          channel: "#cohhcarnage"
      end
      def start_link(client, state \\ %State{}) do
        GenServer.start_link(__MODULE__, [%{state | client: client}])
      end
      def init([state]) do
        ExIrc.Client.add_handler state.client, self
        ExIrc.Client.connect! state.client, state.host, state.port
        {:ok, state}
      end
      def handle_info({:connected, server, port}, state) do
        Logger.debug(state.nick <> " " <> state.pass)
        Client.logon(state.client, state.pass, state.nick, state.nick, state.nick)
        {:noreply, state}
      end
      def handle_info(:logged_in, config) do
        Client.join(config.client, config.channel)
        {:noreply, config}
      end
    end
  end
end
在它的当前状态下,它迟早会崩溃,因为我不在乎IRC随机消息

我需要在文件末尾添加类似的内容来处理所有随机情况:

def handle_info(_msg, state) do
  {:noreply, state}
end

您可以在编译前将catch all
句柄\u info
插入
@钩子中:

编译前的
@
在编译模块之前将调用的钩子

接受模块或元组
{,}
。这个 函数/宏必须有一个参数:模块环境。如果是 一个宏,其返回值将被注入模块的末尾 编译开始前的定义

仅提供模块时,假定函数/宏为 编译前的代码

注意:与编译后的
@不同,回调函数/宏必须是
放置在单独的模块中(因为调用回调时
当前模块尚不存在)

例子

例如:

defmodule MyGenServer do
  defmacro __using__(_) do
    quote do
      use GenServer
      @before_compile MyGenServer

      def start_link do
        GenServer.start_link(__MODULE__, [])
      end
    end
  end

  defmacro __before_compile__(_) do
    quote do
      def handle_info(message, state) do
        IO.inspect {:unknown_message, message}
        {:noreply, state}
      end
    end
  end
end

defmodule MyServer do
  use MyGenServer

  def handle_info(:hi, state) do
    IO.inspect {:got, :hi}
    {:noreply, state}
  end
end

{:ok, pid} = MyServer.start_link
send(pid, :hi)
send(pid, :hello)
:timer.sleep(100)
输出:

{:got, :hi}
{:unknown_message, :hello}

这可能不是您想要的答案,但您试图实现的是我将其归类为代码气味的东西

Elixir以其直白而自豪。调试一段代码时,我可以查看源代码并查看流程。如果此模块中未定义函数,我可以检查文件开头的
使用
查找此函数的定义位置。在您的示例中,当调试不适合其他类型的消息时,我会非常困惑代码没有抛出
FunctionClause

相反,我建议将此添加到
Cgas.IrcServer
中:

  def handle_info({:connected, server, port}, state) do
    ...
  end
  def handle_info(:logged_in, config) do
    ...
  end
  def handle_info({_type, msg, %ExIrc.SenderInfo{user: "cohhilitionbot"} , _channel} = msg, state) do
    do_handle_info(msg, state)
  end
  def handle_info(_msg, state) do
    {:noreply, state}
  end
在您的模块
Cgas.GiveAwayMonitor
中,而不是定义
handle\u info
define
do\u handle\u info

def do_handle_info({_type, msg, %ExIrc.SenderInfo{user: "cohhilitionbot"} , _channel}, state) do
  if String.downcase(msg) |> String.contains?("giveaway") do
    IO.inspect msg
  end
  {:noreply, state}
end
def handle_info(msg, state) do
  try do
    do_handle_info(msg, state)
  rescue
    e in FunctionClauseError -> {:noreply, state}
  end
end
此解决方案的一个缺点是,您至少需要提前了解函数。如果您不知道,您可以在最后一个
句柄\u info
中执行类似操作:

def do_handle_info({_type, msg, %ExIrc.SenderInfo{user: "cohhilitionbot"} , _channel}, state) do
  if String.downcase(msg) |> String.contains?("giveaway") do
    IO.inspect msg
  end
  {:noreply, state}
end
def handle_info(msg, state) do
  try do
    do_handle_info(msg, state)
  rescue
    e in FunctionClauseError -> {:noreply, state}
  end
end

我觉得它比将函数子句注入到模块中要简单一些,并且它实现了相同的结果:您不必重复自己的操作。

下面将是解决此问题的另一种方法:您可以更进一步,直接在
use
中定义额外的
handle\u info
匹配:

defmodule M do

  defmacro __using__(opts) do
    quote bind_quoted: [his: opts |> Keyword.get(:handle_infos, [])] do
      def handle_info(list) when is_list(list) do
        IO.puts "[OPENING] clause matched"
      end

      for {param, fun} <- his do
        def handle_info(unquote(param)), do: (unquote(fun)).(unquote(param))
      end

      def handle_info(_) do
        IO.puts "[CLOSING] clause matched"
      end
    end
  end
end

defmodule U do
  use M, 
      handle_infos: [
        {"Hello", quote do fn(params) ->
                    IO.puts("[INJECTED] with param #{inspect(params)}")
                  end end}
      ]
end

U.handle_info("Hello")
#⇒ [INJECTED] clause matched with param "Hello"
U.handle_info(["Hello"])
#⇒ [OPENING] clause matched
U.handle_info("Hello1")
#⇒ [CLOSING] clause matched
U.handle_info("Hello")
#⇒ [INJECTED] clause matched with param "Hello"
defmdo模块
使用(opts)do定义宏
quote bind_quoted:[his:opts |>Keyword.get(:handle_infos,[])]do
def handle_info(list)何时是_list(list)do
IO.puts“[期初]条款匹配”
结束
为了{param,fun}
IO.puts(“[注入]参数#{inspect(params)}”)
结束}
]
结束
U.handle_信息(“你好”)
#⇒[INJECTED]子句与param“Hello”匹配
U.handle_info([“你好”])
#⇒[开始]条款匹配
U.handle_信息(“Hello1”)
#⇒[结束]条款匹配
U.handle_信息(“你好”)
#⇒[INJECTED]子句与param“Hello”匹配

通过这种方式,我们可以更明确地控制与
handle\u info
功能相关的内容。

我将您的答案与Chris McCord的
元编程灵丹妙药
第三章相结合,最后得出以下结论:

defmodule Cgas.IrcServer do
  defmacro __using__(opts) do
    quote do
      Module.register_attribute __MODULE__, :handlers, accumulate: true
      @before_compile Cgas.IrcServer
      #Some code gen
    end
  end
  defmacro expect_message(pattern, do: action) do
    quote bind_quoted: [
      pattern: Macro.escape(pattern, unquote: true),
      action: Macro.escape(action, unquote: true)
    ] do
      @handlers { pattern, action }
    end
  end
  defmacro __before_compile__(_env) do
    quote do
      use GenServer
      #Some important necessary cases
      compile_handlers
      def handle_info(message, state) do
        IO.inspect({:id_does_not_work, message})
        {:noreply, state}
      end
    end
  end
  defmacro compile_handlers do
    Enum.map(Module.get_attribute(__CALLER__.module, :handlers), fn ({head , body}) ->
      quote do
        def handle_info(unquote(head), state) do
          unquote(body)
          {:noreply, state}
        end
      end
    end)
  end
end
以及一个示例客户机模块

defmodule Cgas.GiveAwayMonitor do
  use Cgas.IrcServer,
    nick: "twitchsniperbot",
    password: "token"

  expect_message { _type, msg  , %ExIrc.SenderInfo{user: "cohhilitionbot"} , _channell}  do
    if String.downcase("ms") |> String.contains?("giveaway") do
      IO.inspect "ms"
    end
  end

end

我认为这很好,因为现在每个handle_info子句都被分组在一起,它有一个默认的catch子句,它有点漂亮,状态我不关心,但底层客户端需要它,它是自动传递的。

为什么有用?
@before\u compile
在用户在模块中编写的所有实际代码之后注入代码。好的。我马上测试一下。编辑:就像一个charm@Haito虽然这个答案可行,但我提出了另一种方法来解决这个问题,这可能会导致代码更易于调试。@Haito同时,在模块中添加任何其他函数(如private helper)可能会破坏上述解决方案中的代码。我认为这可能不太好,因为在添加新功能时,我必须不断更改do_handle_info sig。现在我只需要msg和state,但有一天我可能需要从这个tuple访问其他东西。但我喜欢这种方法。嗯。。很好,但我认为它在语法上有点难看,但我认为我们可能会达到目的。好吧,目的是展示这项技术,一个明确的方法是将引用调用封装到一个方便的宏中,以更可读的方式声明函数等等。我只是想展示,模式。这很好:)您不必记住将handle\u info放在文件末尾,您不必记住所有可用的
handle\u info
子句组合。这是明确的,在阅读模块时,我可以清楚地看到在哪里搜索bug。我不知道为什么在编译之前需要使用
。你不能用这种方法把所有的东西都放到
使用
宏中吗?不,因为在这个过程中,使用@handlers总是为零,因为编译器还没有到达任何预期消息。至少这是我在克里斯的书中读到的。在编译之前,在编译器遍历整个文件之后执行编译。这是有道理的。所以你可能没有比这更简单的了。尼斯:)