让我们说我有一个具有长功能的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 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
然后我用这个mixin
defmodule MyModule do
use Mixin
@Impl Mixin
def cb(a, b, c) do
# ...
end
def some_other_fun do
# ...
end
end
问题是,现在MyModule
我可以访问subfunction_1
和subfunction_2
(例如来自some_other_fun
),这是不受欢迎的。此外,如果有人意外地实施了例如subfunction_1
MyModule
中会出现隐藏的错误。由于子函数调用回调,它们或者不能被移出使用,或者模块名称需要传递给它们,这是一个丑陋的解决方案。
有什么办法吗?
答案 0 :(得分:3)
首先让我们澄清一下符号:您正在使用模块来提供默认的回调实现。谈论mixins有点奇怪,因为你没有混合模块内容,而是生成新的东西。
通常,在用户模块中生成大量内容是一种不好的做法,这完全是因为您提出的所有问题。相反,您应该调用一个传递所有相关参数的函数,如下所示:
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_function
模块中定义Mixin
,它可以在任何地方。这个想法只是为了保持生成的代码简短。它也应该有助于单元测试,因为您可以直接测试long_function
而无需生成大量模块。
我们将讨论此主题in the Macro guides too。我还可以推荐Chris McCord的Metaprogramming书。
答案 1 :(得分:1)
扩展现有答案:重构您给出的问题代码:
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_function(args)
。
如果非玩具示例有用,请查看create
方法和create_changeset
回调如何在我的库BaseModel
中进行交互,后者使用此确切模式。