在 __using__ 宏中添加默认值 handle_info
Adding default handle_info in __using__ macro
我正在尝试围绕 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
您可以在 @before_compile
挂钩中注入包罗万象的 handle_info
:
@before_compile
A hook that will be invoked before the module is compiled.
Accepts a module or a tuple {<module>, <function/macro atom>}
. The
function/macro must take one argument: the module environment. If it’s
a macro, its returned value will be injected at the end of the module
definition before the compilation starts.
When just a module is provided, the function/macro is assumed to be
__before_compile__/1
.
Note: unlike @after_compile
, the callback function/macro must be
placed in a separate module (because when the callback is invoked, the
current module does not yet exist).
Example
defmodule A do
defmacro __before_compile__(_env) do
quote do
def hello, do: "world"
end
end
end
defmodule B do
@before_compile A
end
示例:
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 以非常明确而自豪。调试一段代码时,我可以查看源代码并查看流程。如果此模块中未定义函数,我可以检查文件开头的 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
我觉得它比将函数子句注入模块要简单一些,而且它实现了相同的结果:您不必重复自己。
下面是解决这个问题的另一种方法:可以更进一步,直接在 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
函数相关的内容。
我将你的答案与 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 子句,它有点漂亮,状态我不关心,但底层客户端需要它是自动传递的.
我正在尝试围绕 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
您可以在 @before_compile
挂钩中注入包罗万象的 handle_info
:
@before_compile
A hook that will be invoked before the module is compiled.
Accepts a module or a tuple
{<module>, <function/macro atom>}
. The function/macro must take one argument: the module environment. If it’s a macro, its returned value will be injected at the end of the module definition before the compilation starts.When just a module is provided, the function/macro is assumed to be
__before_compile__/1
.Note: unlike
Example@after_compile
, the callback function/macro must be placed in a separate module (because when the callback is invoked, the current module does not yet exist).defmodule A do defmacro __before_compile__(_env) do quote do def hello, do: "world" end end end defmodule B do @before_compile A end
示例:
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 以非常明确而自豪。调试一段代码时,我可以查看源代码并查看流程。如果此模块中未定义函数,我可以检查文件开头的 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
我觉得它比将函数子句注入模块要简单一些,而且它实现了相同的结果:您不必重复自己。
下面是解决这个问题的另一种方法:可以更进一步,直接在 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
函数相关的内容。
我将你的答案与 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 子句,它有点漂亮,状态我不关心,但底层客户端需要它是自动传递的.