在__using__宏中添加默认的handle_info

时间:2016-09-14 12:53:48

标签: macros metaprogramming elixir

我试图围绕ExIrc制作一个小包装,但我遇到了一些问题。 __using__宏将ast附加到模块的开头,我想将函数定义作为默认值handle_info附加到它的最后。我可以在使用该包装器的每个模块中手动完成它,但我确信在某些时候我会忘记它。

我目前的包装器实现:

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

使用它的示例模块:

defmodule Cgas.GiveAwayMonitor do
  use Cgas.IrcServer,
    nick: "twitchsniperbot",
    password: "token"
  require Logger
  def 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
end

在目前的状态下,由于我不在乎的IRC随机消息,它迟早会崩溃。

我需要在文件末尾追加一些东西以处理所有随机情况:

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

4 个答案:

答案 0 :(得分:5)

您可以在handle_info挂钩中注入全部@before_compile

  

@before_compile

     

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

     

接受模块或元组{<module>, <function/macro atom>}。该   function / macro必须带一个参数:模块环境。如果它是   一个宏,它的返回值将在模块的末尾注入   编译开始前的定义。

     

当只提供模块时,假设函数/宏为   __before_compile__/1

     

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

     例
defmodule A do
  defmacro __before_compile__(_env) do
    quote do
      def hello, do: "world"
    end
  end
end

defmodule B do
  @before_compile A
end

Source

示例:

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}

答案 1 :(得分:1)

以下是解决此问题的另一种方法:可以更进一步,直接在handle_info内定义其他use个匹配项:

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"

这样可以更明确地控制与handle_info函数相关的内容。

答案 2 :(得分:1)

我已将你的答案与Chris McCord的Metaprogramming Elixir的第3章结合起来,我最终得到了这个:

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子句,它有点漂亮和状态,我不关心,但底层客户端需要自动传递。

答案 3 :(得分:0)

这可能不是您正在寻找的答案,但您想要达到的目标是我将其归类为代码气味。

Elixir非常自豪。在调试一段代码时,我可以查看源代码并查看流程。如果在此模块中没有定义函数,我可以检查use文件的开头,以查找定义此函数的位置。在您的示例中,在调试不适合其他类型的消息时,我会非常困惑,代码不会抛出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_info定义do_handle_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

此解决方案的一个缺点是您至少需要知道功能。如果您不知道,可以在上一个handle_info中执行类似的操作:

def handle_info(msg, state) do
  try do
    do_handle_info(msg, state)
  rescue
    e in FunctionClauseError -> {:noreply, state}
  end
end

我觉得在将模块中的函数子句注入模块时会有点不那么hacky,它会达到相同的结果:你不必重复自己。