【问题标题】:Adding default handle_info in __using__ macro在 __using__ 宏中添加默认的 handle_info
【发布时间】:2017-01-22 06:59:25
【问题描述】:

我正在尝试围绕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

【问题讨论】:

    标签: macros metaprogramming elixir


    【解决方案1】:

    您可以将包罗万象的handle_info 注入@before_compile 挂钩:

    @before_compile

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

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

    当只提供一个模块时,函数/宏被假定为 __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}
    

    【讨论】:

    • 为什么会有帮助?
    • @before_compile 在用户在模块中编写的所有实际代码之后注入代码。
    • 好的。我马上测试一下。编辑:像魅力一样工作
    • @Haito 虽然这个答案可行,但我提出了另一种方法来解决这个问题,这可能会导致代码更容易调试。
    • @Haito 另外,将任何其他函数(如私有助手)放入模块中可能会破坏上述解决方案中的代码。
    【解决方案2】:

    这可能不是您正在寻找的答案,但您试图实现的目标是我将其归类为代码异味的东西。

    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,它达到了相同的结果:你不必重复自己。

    【讨论】:

    • 我认为这可能效果不佳,因为当我要添加新功能时,我必须不断更改 do_handle_info sig。现在我只需要消息和状态,但有一天我可能需要从那个元组访问其他东西。但我喜欢这种方法。
    【解决方案3】:

    下面将是解决此问题的另一种方法:可以更进一步,直接在 use 内定义额外的 handle_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"
    

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

    【讨论】:

    • 嗯.. 很好,但我认为它在语法上有点难看,但我认为我们可能会到达那里。
    • 嗯,目的是展示这项技术,人们肯定可以将引用的调用包装成一个方便的宏,以更易读的方式声明函数等。我只是想展示模式。
    【解决方案4】:

    我已将您的答案与 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 子句,它有点漂亮,并且我不关心但底层客户端需要它的状态会自动传递。

    【讨论】:

    • 太好了 :) 您不必记住将 handle_info 放在文件末尾,您不必记住所有可用的 handle_info 子句组合。这是明确的,当阅读模块时,我可以清楚地看到在哪里搜索错误。我不确定你为什么需要使用before_compile。你不能用这种方法把所有东西都放在using 宏中吗?
    • 不,因为在 using 中@handlers 总是为零,因为编译器还没有达到任何期望消息。至少我在克里斯的书中读到的。而 before_compile 在编译器遍历整个文件后执行。
    • 这是有道理的。所以你可能没有比这更简单的了。不错:)
    猜你喜欢
    • 1970-01-01
    • 2011-01-20
    • 2017-08-13
    • 1970-01-01
    • 2022-08-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多