Elixir 使用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

假设我有一个长函数的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
因此,对于可读性相关的内容,我将其拆分为更小的函数:

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()
语法。。。。这不是一个完美的解决方案,但现在您只有一个运行时错误源,其余的将在编译时进行检查。只有一个,而且一个太多;)我知道这些运行时错误不太可能发生,并且可以很容易地调试,这就是为什么我认为这是一个不错的解决方案(而且是经过投票的),只是看起来不太好