你如何扩展/继承一个长生不老药模块?

Posted

技术标签:

【中文标题】你如何扩展/继承一个长生不老药模块?【英文标题】:How do you extend/inherit an elixir module? 【发布时间】:2016-05-20 00:54:29 【问题描述】:

假设一个 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 中做到这一点?

【问题讨论】:

比如我需要在github.com/CargoSense/ex_aws/blob/…处添加def decode(%"NULL" => true), do: nilExAws.Dynamo.Decoder 这是实现中的错误吗?如果是这样,在修复发布之前制作 PR 并使用自己的 fork 怎么样? 这样做。想知道长生不老药中的一般解决方案是什么样的。据我所知,没有一个干净的方法可以在不重写原始库的情况下仅使用一个函数来扩展模块。 elixir 应该考虑添加一个includes/extends 这只是递归函数实现中断的特定情况下的问题,您无法控制整个源代码。 OO 通过继承解决的大多数其他问题实际上都可以在 Elixir 中非常优雅地解决。也就是说,即使 OO 也不允许您像在这里尝试那样添加子句,还是我弄错了? 【参考方案1】:

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

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

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

【讨论】:

这不起作用,因为 Decoder.decode(x) 将无法调用 MyDecoder.decode,因为它通过递归/委托工作。【参考方案2】:

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

但是,在这种特殊情况下,这就像用大锤打苍蝇一样。我的意思是你可能会重写代码以使用协议,但如果你想简单地扩展解析器,然后分叉代码并进行修改。哦,别忘了把 PR 发回给原来的开发者,因为他可能也想得到你的修复。

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

一般来说,在 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?)

注意:我特意保持代码非常简单以便说明问题。这可能不是有效的语法。我还没有检查,我现在不能。

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

【讨论】:

我认为最初的问题是指他不想接触的外部库。 如果他指的是外部图书馆,他的问题并不清楚。【参考方案3】:

显然,你可以。看看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

【讨论】:

【参考方案4】:

也许defdelegate 可以解决问题:

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

  defdelegate decode(map), to: Decoder
end

【讨论】:

以上是关于你如何扩展/继承一个长生不老药模块?的主要内容,如果未能解决你的问题,请参考以下文章

审查长生不老药中字符串的第一个和最后一个字符之间的字符

长生不老药函数中的“&1”是啥意思?

json 长生不老药的VS代码任务

正经“长生不老药”新进展:口服那种,贝佐斯投资 | 柳叶刀子刊

Elixir Simple 模块仅产生“参数错误”

衡阳拜佛