redcodefinal
redcodefinal

Reputation: 909

How to resolve ruby module/mixin method conflict

In the following, A inherits F, which inherits E, so calling initialize on an A instance calls A#initialize, which has priority over E#initialize and F#initialize.

module E
  def initialize(e)
    @e = e
  end

  def e
    @e
  end
end

module F
  def initialize(f)
    @f = f
  end

  def f
    @f
  end
end

class A
  include E
  include F

  def initialize(e, f)
    # ...
  end
end

I need to refer to both E#initialize and F#initialize from within the method body of A#initialize, passing e and f respectively as the argument so that I get this result:

a = A.new("foo", "bar")
a.e # => "foo"
a.f # => "bar"

Is there a way to refer to those methods?

Upvotes: 0

Views: 504

Answers (2)

Cary Swoveland
Cary Swoveland

Reputation: 110675

You can use Method#super_method for this.

module E
  def initialize(e)
    @e = e
  end
  def e
    @e
  end
end

module F
  def initialize(f)
    @f = f
  end

  def f
    @f
  end
end

class A
  include E
  include F

  def initialize(e, f)
    select_initialize(E).call e
    select_initialize(F).call f
  end

  private

  def select_initialize(mod) 
    self.class.
         ancestors.
         index(mod).
         times.
         reduce(method(:initialize)) { |m,_| m.super_method }
  end
end

puts A.new("E", "F").f
  #=> F
puts A.new("E", "F").e
  #=> E

Note:

A.ancestors
  #=> [A, F, E, Object, Kernel, BasicObject] 

See also Module#ancestors, Array#index, Integer#times, Enumerable#reduce (aka inject), Object#method and Method#call.

E and F may of course contain other instance methods that are needed by instances of the class.

Upvotes: 3

Amadan
Amadan

Reputation: 198324

The problem for you is that there is no multiple inheritance in Ruby; so the modules get inserted as an ancestor one above the other, and as such F#initialize overshadows E#initialize. As you found, it is easy to access F#initialize using super(f); but the other one needs a hack to access: we can pick the initialize method directly off of the module, because it is an ancestor; then bind it to the current object and run it.

def initialize(e, f)
  E.instance_method(:initialize).bind(self).call(e)
  F.instance_method(:initialize).bind(self).call(f) # equivalent to super(f)
end

However, I do not recommend this. If you need to run several initialisers, you'd be better off using composition, rather than inheritance (and to be clear, include is inheritance).

Upvotes: 4

Related Questions