如何扩展/继承 Elixir 模块?

问题描述 投票:0回答:5

假设一个 Elixir 库定义了:

defmodule Decoder do

  def decode(%{"BOOL" => true}),    do: true
  def decode(%{"BOOL" => false}),   do: false
  def decode(%{"BOOL" => "true"}),  do: true
  def decode(%{"BOOL" => "false"}), do: false
  def decode(%{"B" => value}),      do: value
  def decode(%{"S" => value}),      do: value
  def decode(%{"M" => value}),      do: value |> decode
  def decode(item = %{}) do
    item |> Enum.reduce(%{}, fn({k, v}, map) ->
      Map.put(map, k, decode(v))
    end)
  end
end

我想定义一个模块

MyDecoder
,它只是在上面的模块中添加一个
def decode
。 在 oo 语言中,这可以通过某种继承/混合/扩展来完成。

我如何在 Elixir 中做到这一点?

elixir
5个回答
21
投票

有一种机制可以扩展模块的行为。 这称为协议。 您可以在此处找到更多信息。 您可以将 Elixir 协议视为类似于 OO 中的接口。

但是,在这种特殊情况下,这就像用大锤打苍蝇一样。我的意思是,您可能可以重写代码以使用协议,但如果您想简单地扩展解析器,则分叉代码并进行修改。 哦,别忘了将 PR 发送回原始开发人员,因为他可能也希望修复您的问题。

有时最简单的答案就是最好的答案。 即使这是面向对象的代码,如果某些开发人员继承了该类或类似的东西,我也会在代码审查中标记它。 为什么? 因为继承会导致病态的代码耦合

一般来说,在 FP 中(请注意,我在这里做了一个大的概括),我们通常扩展行为的方式是通过高阶函数。 也就是说,如果我们想要不同的行为,我们就不会使用多态性;而是使用多态性。我们只需将我们想要的行为直接传递给高阶函数即可。 当我说“通过这种行为”时,我的意思是什么? 考虑一下我有一些验证代码,例如:

defmodule V do
  def is_odd?(v) do
    rem(v,2) != 0
  end
end

defmodule T do
   def is_valid_value?(v, f) do
     if f(v), do: true, else: false
   end
end

在其他地方我会有

T.is_valid_value?(myvalue, V.is_odd?)
。 突然我的客户意识到,他们需要检查该值是否大于 100,而不是检查该值是否为奇数。所以我会按照以下方式做一些事情:

defmodule V do
  def greater_than_100?(v) do
    v > 100
  end
end

然后我会将我的电话更改为:

T.is_valid_value?(myvalue, V.greater_than_100?)

注意:我故意保持代码非常简单以表达观点。这可能不是有效的语法。我还没查过,现在也查不到。

就是这样。就这样。聪明的开发人员可能会不同意,但对我来说,这比继承行为并覆盖它更简单、更容易遵循。


10
投票

显然,你可以。看一下this gist,它使用一些相当“晦涩”的方法来列出模块的公共函数,然后从中生成委托。真是太酷了。

这就是全部内容:

defmodule Extension do
  defmacro extends(module) do
    module = Macro.expand(module, __CALLER__)
    functions = module.__info__(:functions)
    signatures = Enum.map functions, fn { name, arity } ->
      args = if arity == 0 do
               []
             else
               Enum.map 1 .. arity, fn(i) ->
                 { binary_to_atom(<< ?x, ?A + i - 1 >>), [], nil }
               end
             end
      { name, [], args }
    end
    quote do
      defdelegate unquote(signatures), to: unquote(module)
      defoverridable unquote(functions)
    end
  end
end

你可以像这样使用它:

defmodule MyModule do
   require Extension
   Extension.extends ParentModule
   # ...
end

不幸的是,它对最新的 Elixir 版本发出了警告,但我确信这是可以解决的。除此之外,它就像一个魅力!

编辑以免引发警告:

defmodule Extension do
  defmacro extends(module) do
    module = Macro.expand(module, __CALLER__)
    functions = module.__info__(:functions)
    signatures = Enum.map functions, fn { name, arity } ->
      args = if arity == 0 do
               []
             else
               Enum.map 1 .. arity, fn(i) ->
                 { String.to_atom(<< ?x, ?A + i - 1 >>), [], nil }
               end
             end
      { name, [], args }
    end

    zipped = List.zip([signatures, functions])
    for sig_func <- zipped do
      quote do
        defdelegate unquote(elem(sig_func, 0)), to: unquote(module)
        defoverridable unquote([elem(sig_func, 1)])
      end
    end
  end
end

8
投票

也许

defdelegate
可以解决问题:

defmodule MyDecoder do
  def decode(%{"X" => value}), do: value

  defdelegate decode(map), to: Decoder
end

0
投票

如果您不控制原始模块,我不确定是否有一个简单的解决方案。也许您可以尝试递归地预处理数据,然后将结果提供给原始实现。

如果您可以控制原始模块,则一种方法是将公共子句提取到宏中,然后在实际的解码器模块中使用它:

defmodule Decoder.Common do
  defmacro __using__(_) do
    quote do
      def decode(%{"BOOL" => true}),    do: true
      def decode(%{"BOOL" => false}),   do: false
      def decode(%{"BOOL" => "true"}),  do: true
      def decode(%{"BOOL" => "false"}), do: false
      def decode(%{"B" => value}),      do: value
      def decode(%{"S" => value}),      do: value
      def decode(%{"M" => value}),      do: value |> decode
      def decode(item = %{}) do
        item |> Enum.reduce(%{}, fn({k, v}, map) ->
          Map.put(map, k, decode(v))
        end)
      end
    end
  end
end

defmodule Decoder do
  use Decoder.Common
end

defmodule MyDecoder do
  def decode(%{"FOO" => value}), do: "BAR"

  use Decoder.Common
end

0
投票

当前的解决方案已经过时;所以这是一个新的。添加了

print
函数来展示
super
的使用,它调用默认实现。

defmodule Extension do
  defmacro extends(module) do
    module = Macro.expand(module, __CALLER__)

    # Gets list of public functions from module as list 
    # of {atom, int} tuples - does not get private functions
    functions = module.__info__(:functions)

    for {name, arity} <- functions do
     # generate `arity` anonymous argument names. Example: [arg1, arg2, arg3]
      arguments = Macro.generate_arguments(arity, __MODULE__)

      # unquote_splicing expands the given arguments from
      # `[arg1, arg2, arg3]` to `arg1, arg2, arg3`. Using unquote alone 
      # would cause the function to only accept 1 argument being a list 
      # with `arity` elements as the code would be `my_function([arg1, arg2, arg3])`
      # instead of `my_function(arg1, arg2, arg3)`
      quote do
        defdelegate unquote(name)(unquote_splicing(arguments)), to: unquote(module)
        defoverridable [{unquote(name), unquote(arity)}]
      end
    end
  end
end

defmodule BaseModule do
  @variable 5

  def greet do
    "Hello from BaseModule!"
  end

  def farewell do
    "Goodbye from BaseModule!"
    BaseModule.aloha()
  end

  def aloha do
    "Aloha from BaseModule! @variable: " <> inspect(@variable) <> " " <> private_fn()
  end

  def multiply(val1, val2) do
    val1 * val2
  end

  defp private_fn() do
    "(This is private)"
  end

  def print(val1), do: IO.puts "#{val1} from BaseModule"
end

defmodule ChildModule do
  # Use the BaseModule functions
  require Extension
  Extension.extends(BaseModule)

  # Override the greet function
  def greet do
    "Hello from ChildModule!"
  end

  def print(val1) do
    IO.puts "#{val1} from ChildModule"
    super(val1)
  end
end
© www.soinside.com 2019 - 2024. All rights reserved.