【问题标题】:Merge two Ruby modules but not include合并两个 Ruby 模块但不包含
【发布时间】:2012-07-29 09:12:27
【问题描述】:

我想在不破坏查找链的情况下合并两个 Ruby 模块。基本上我希望BothAnB 的行为就像我将A 和B 的文本源代码连接起来并且新的foo 替换旧的一样。当 MRO 线性化继承菱形时会出现问题。

module O
  def foo; puts "O" end
end

module A
  include O
  def foo; puts "A"; super end
  def aaa; puts "aaa" end
end

module B
  include O
  def foo; puts "B"; super end
  def bbb; puts "bbb" end
end

module BothAnB
  #insert magic here such that a class C that includes BothAnB:
  # C.new.foo => B O
  # C.new.aaa => aaa
  # C.new.bbb => bbb
end

module JustA
  #insert magic here such that a class C that includes JustA:
  # C.new.foo => A O
  # C.new.aaa => aaa
  # C.new.bbb => FAIL
end
#and similarly JustB

A 和 B 是相当复杂的模块,可以有很深的继承链(这是一个允许程序员这样做的元编程框架)。

Include B, A 不起作用,因为不是查找 BothAnB->B->A->O,我需要它是 BothAnB->B->O(和可选的 ->A)。 我已经接近了:

  • 深度克隆 A 的整个继承树(去除菱形)
  • undef_method 在 A 的克隆上删除在 B 中找到的方法
  • Module 制定新方法以反映此行为

还有比这更好的解决方案吗?理想情况下,我希望在调用 BothAnB.ancestors 时至少保留一些可识别的模块。

[注意:在根据 Phrogz 的反馈得到两个答案后,我完全改变了问题,所以如果它们看起来无关紧要]

【问题讨论】:

  • 为什么要这样做?这闻起来像XY problem。正如所写,你想做的事情听起来是个坏主意。
  • 我正在构建一个元编程框架,这样逻辑就消失了。 :( 实际代码看起来(或工作)不像这样

标签: ruby module mixins method-resolution-order


【解决方案1】:
M1parent.send(:remove_method, :foo)

您必须将其从 M1parent 远程,因为这是定义它的地方,例如 M1.send(:remove_method, :foo),因为方法 foo 是在 M1parent 上定义的,所以它不起作用。

【讨论】:

  • 有什么替代方法吗? M1 非常复杂(许多包含其他模块的包含模块)只是因为它的作用。加上它的父母是共享的,所以我不能直接在那里删除它
【解决方案2】:

这会为你解决吗?

module M1
  def foo; 42; end
  def bar; 17; end
end

class Base
  def foo; 0; end
end

require 'remix' # gem install remix 
class X < Base
  include_after Base, M1
end

p X.new.foo, #=> 0
  X.new.bar  #=> 17

【讨论】:

  • 是的,这看起来正是我所需要的!但它似乎不适用于 Ruby 1.9
  • @alexloh 我在 Windows 上的 Ruby 1.9.3 上运行了上面的代码。您在哪个确切的 Ruby 版本和操作系统上进行了尝试,您遇到了什么错误?可以肯定的是:您是否安装了remix gem
  • 我无法安装 gem。 “错误:无法构建 gem 原生扩展”我在 Ubuntu 上使用 Ruby 1.9.3
【解决方案3】:

我的建议如下:登录http://bugs.ruby-lang.org/ 并提交功能请求,或者,如果您更有信心,可以提交错误请求。因为这基本上是Ruby的问题。当前的 Ruby 行为在没有经验的情况下是出乎意料的,因此应该更改为预期的行为:这样您就可以通过简单的调用来获得所需的内容

module BothAnB
  include A
  include B
end

正如您自己指出的那样,解决方法肯定是可能的。但是这个恕我直言的 Ruby 行为是不正确的。

所以在此之前,您必须避免调用 super,我建议您改为玩

O.instance_method( :foo )

并从 B 和 A 模块调用它,而不是方便的关键字 super:

module A
  include O
  def foo
    puts "A"
    O.instance_method( :foo ).bind( self ).call
  end
end

# do same for B and it'll work

【讨论】:

  • 我不确定这是否应该被视为一个错误。线性化迫使 O 在 A 之前,否则当 B 中未定义 foo 时,X 会中断。我确信这不是他们容易做出的设计决定,其他语言,尤其是 Scala 特征,具有不同的 mixin 语义,而 Python afaik 与红宝石。但是,是的,我同意这是出乎意料的。
  • 感谢您的启发性评论。
【解决方案4】:

这是另一个可能的技巧:

module BothAnB
  include A
  include O.clone
  include B
end

class C
  include BothAnB
end

C.new.foo
C.new.aaa
C.new.bbb

# output:
B
O
aaa
bbb

这里我们将B#foo 中的super 指向O#foo 而不是A#foo

如果O 很复杂并且包含其他内容,则可能需要更多这样的魔法:

module O
  # don't do this:
  # include Whatever

  # do this instead:
  def self.included(base)
    base.send(:include, Whatever.clone)
  end
end

【讨论】:

  • 好的。这不是我理想的解决方案,但它是克隆的糖,我想我将得到尽可能接近。我需要输入include Whatever 吗?什么时候调用回调included?我尝试了几次(根据手册)但失败了
  • 在进行一些棘手的元编程或内部 DSL 时,碰壁是很常见的,这使您使用宿主语言的脆弱细微差别。有时理想的解决方案是一开始就没有问题。但是,如果重新考虑设计约束是不可行的,那么您最终会在或多或少的邪恶方法之间做出选择。我的方法是那些邪恶的方法之一。我认为,理想的解决方案将依赖于一些明确的语言结构。
  • 回到实用方面:不,您不需要输入include Whatever,为了清楚起见,我已经更新了答案。有关回调的提示,请参阅gist.github.com/3413487
猜你喜欢
  • 2021-12-18
  • 1970-01-01
  • 1970-01-01
  • 2016-02-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多