Elixir 使用mixin在模块中隐藏私有函数
假设我有一个长函数的mixin,如下所示:Elixir 使用mixin在模块中隐藏私有函数,elixir,mixins,Elixir,Mixins,假设我有一个长函数的mixin,如下所示: defmodule Mixin do @callback cb ... defmacro __using__(_) do quote do def long_function do #a lot of code cb(a, b, c) #even more code end end end end defmo
defmodule Mixin do
@callback cb ...
defmacro __using__(_) do
quote do
def long_function do
#a lot of code
cb(a, b, c)
#even more code
end
end
end
end
defmodule Mixin do
@callback long_function(...) :: ...
defmacro __using__(_) do
quote do
def long_function(arg1, arg2) do
Mixin.long_functtion(arg1, arg2)
end
end
end
def long_function(arg1, arg) do
... actual implementation ...
end
end
因此,对于可读性相关的内容,我将其拆分为更小的函数:
defmodule Mixin do
@callback cb ...
defmacro __using__(_) do
quote do
def long_function do
subfunction_1()
subfunction_2()
end
defp subfunction_1 do
#some code
cb(a, b, c)
end
defp subfunction_2 do
#some code
cb(a, b, c)
#some code
end
end
end
end
然后我用这个混合器
defmodule MyModule do
use Mixin
@Impl Mixin
def cb(a, b, c) do
# ...
end
def some_other_fun do
# ...
end
end
问题是,现在在MyModule
中,我可以访问子功能\u 1
和子功能\u 2
(例如,从一些其他功能
),这是不需要的。此外,如果有人意外地在MyModule
中实现了例如子功能1
,则会出现隐藏的bug。当子函数调用回调时,要么不能将它们移出使用状态,要么需要将模块名传递给它们,这是一个丑陋的解决方案
有什么方法可以做到这一点吗?首先,让我们澄清一下表示法:您正在使用一个模块来提供默认回调实现。谈论mixin有点奇怪,因为您不是在混合模块内容,而是在生成新的内容 一般来说,在用户模块中生成大量内容是一种不好的做法,正是因为您提出的所有问题。相反,您应该调用一个传递所有相关参数的函数,如下所示:
defmodule Mixin do
@callback cb ...
defmacro __using__(_) do
quote do
def long_function do
#a lot of code
cb(a, b, c)
#even more code
end
end
end
end
defmodule Mixin do
@callback long_function(...) :: ...
defmacro __using__(_) do
quote do
def long_function(arg1, arg2) do
Mixin.long_functtion(arg1, arg2)
end
end
end
def long_function(arg1, arg) do
... actual implementation ...
end
end
long_函数
不需要在Mixin
模块中定义,它可以在任何地方。这样做的目的只是让生成的代码保持简短。它也应该有助于单元测试,因为您可以直接测试long\u函数
,而无需生成大量模块
我们讨论这个话题。我还可以推荐Chris McCord的元编程书。扩展现有答案:重构您给出的问题代码:
defmodule Mixin do
@callback cb ...
defmacro __using__(_) do
quote do
def long_function(args) do
# you must use the module.fun syntax here, as this code
# will be injected into the host
# Also, pass __MODULE__ so that the Mixin functions can find the callback
Mixin.long_function(__MODULE__, args)
end
end
end
# host_module is where `use Mixin` appears, and where `cb` is implemented
def long_function(host_module, args) do
subfunction_1(host_module, args)
subfunction_2(host_module, args)
end
defp subfunction_1(host_module, args) do
#some code
cb(host_module, a, b, c)
end
defp subfunction_2(host_module, args) do
#some code
cb(host_module, a, b, c)
#some code
end
defp cb(hostmodule, a, b, c) do
# This is a potential source of runtime errors, you must
# manually check that it is being called correctly:
hostmodule.cb(a, b, c)
end
end
def MyModule do
use Mixin
# expands to:
# def long_function(args)
@impl Mixin
def cb(a, b, c), do: :something
end
现在您有了JoséValim提到的好处:有趣的代码是纯函数,可以轻松测试,宏代码非常小。创建一个非常简单、非常可预测的CB模块版本将允许简单地测试混合功能/子功能。您还可以获得所需的语法:能够调用MyModule.long_函数(args)
,而不会到处泄漏子函数
如果一个非玩具示例有用,请查看
create
方法和create\u changeset
回调在我的库中是如何交互的,我的库使用了这种精确的模式。感谢您的回答,问题是在我的例子中,mixin或其他任何东西正在为回调提供抽象,因此,它们经常在long_函数中执行。这也意味着long_函数只有在实现缺少回调的情况下才有意义,单独测试它是没有意义的。我认为答案仍然是一样的。将逻辑从生成的代码中移出,移到只包含实际实现的公共模块或私有模块。这就是我在最后一句中提到的,称之为“丑陋的解决方案”:P尽管如此,我可能最终会得到它。改进这一点的语言机制会很有用。哪一部分让你觉得难看?当然,传递模块名称。也许这看起来是一个品味的问题,但我认为像扩展模块/提供默认回调这样的事情应该是编译时操作。例如,如果在调用module.function(…)
时出错了函数名或arity,编译器将不会警告您。您希望在主机模块或Mixin中捕获此类错误吗?如果您正在使用@impl,那么在主机中定义cb
时,应该会警告您算术不匹配;在Mixin中,您可以向Mixin中添加一个函数,以便在每个回调的一个位置只使用module.function()
语法。。。。这不是一个完美的解决方案,但现在您只有一个运行时错误源,其余的将在编译时进行检查。只有一个,而且一个太多;)我知道这些运行时错误不太可能发生,并且可以很容易地调试,这就是为什么我认为这是一个不错的解决方案(而且是经过投票的),只是看起来不太好