Merge two Ruby modules but not include
-
23-06-2021 - |
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]
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