Frage

I want to merge two Ruby modules without breaking the lookup chain. Basically I want the behavior of BothAnB to be exactly as if I concatenated the textual source code from A and B and the new foo replaces the old. The problem occurs when MRO linearizes an inheritance diamond.

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 and B are fairly complex modules that can have deep inheritance chains (this is for a meta-programming framework that allows programmers to do just that).

Include B, A doesn't work because instead of the lookup BothAnB->B->A->O, I need it to be BothAnB->B->O(and optionally ->A). I've gotten close by:

  • deep cloning entire inheritance tree of A (to remove diamond)
  • undef_method on the A's clone to remove methods found in B
  • making a new method for Module to reflect this behavior

Is there a better solution than this? I would ideally want to keep at least some of the modules recognizable when calling BothAnB.ancestors.

[Note: I completely changed the question after getting two answers based on Phrogz's feedback, so if they seem irrelevant they are]

War es hilfreich?

Lösung

Here's another possible trick:

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

Here we make super in B#foo to point at O#foo instead of A#foo.

If O is complex and includes other stuff, it may require more of such magic:

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

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

Andere Tipps

Would this solve it for you?

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
M1parent.send(:remove_method, :foo)

You must remote it from M1parent because that's where it's defined, M1.send(:remove_method, :foo) for example, would not work because the method foo is defined on M1parent.

My suggestion is as follows: Log into http://bugs.ruby-lang.org/ and submit a feature request, or, if you are more confident, a bug request. Because this is basically a problem of Ruby. Current Ruby behavior is unexpected without prior experience and thus should be change to the expected behavior: So that you can get what you need by simply calling

module BothAnB
  include A
  include B
end

Workarounds are surely possible, as you noted yourself. But the Ruby behavior in this imho is not correct.

So until then, you'll have to refrain from calling super and I'd suggest instead playing with

O.instance_method( :foo )

and calling it from B and A modules instead of convenience keyword 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
Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top